@babylonjs-toolkit/server 0.0.0-stage → 9.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/CUSTOM.md +1690 -0
  2. package/DEPLOY.md +2012 -0
  3. package/README.md +1081 -2
  4. package/lib/AbuseLimits.d.ts +64 -0
  5. package/lib/AbuseLimits.js +121 -0
  6. package/lib/Authentication.d.ts +86 -0
  7. package/lib/Authentication.js +272 -0
  8. package/lib/BootSessionOwner.d.ts +86 -0
  9. package/lib/BootSessionOwner.js +116 -0
  10. package/lib/ColyseusLoggerAdapter.d.ts +32 -0
  11. package/lib/ColyseusLoggerAdapter.js +73 -0
  12. package/lib/DedicatedSceneRegistry.d.ts +52 -0
  13. package/lib/DedicatedSceneRegistry.js +74 -0
  14. package/lib/DedicatedSession.d.ts +152 -0
  15. package/lib/DedicatedSession.js +173 -0
  16. package/lib/DualTransport.d.ts +112 -0
  17. package/lib/DualTransport.js +197 -0
  18. package/lib/GameServer.d.ts +64 -0
  19. package/lib/GameServer.js +647 -0
  20. package/lib/GameServerConfig.d.ts +387 -0
  21. package/lib/GameServerConfig.js +1059 -0
  22. package/lib/GameServerLobbyRoom.d.ts +33 -0
  23. package/lib/GameServerLobbyRoom.js +56 -0
  24. package/lib/GameServerLogger.d.ts +111 -0
  25. package/lib/GameServerLogger.js +190 -0
  26. package/lib/GameServerMetrics.d.ts +158 -0
  27. package/lib/GameServerMetrics.js +276 -0
  28. package/lib/GameServerState.d.ts +164 -0
  29. package/lib/GameServerState.js +26 -0
  30. package/lib/HeadlessRuntime.d.ts +316 -0
  31. package/lib/HeadlessRuntime.js +380 -0
  32. package/lib/JoinGate.d.ts +34 -0
  33. package/lib/JoinGate.js +221 -0
  34. package/lib/NetworkAddresses.d.ts +33 -0
  35. package/lib/NetworkAddresses.js +90 -0
  36. package/lib/PlayerTokens.d.ts +17 -0
  37. package/lib/PlayerTokens.js +24 -0
  38. package/lib/RoomServerTransport.d.ts +73 -0
  39. package/lib/RoomServerTransport.js +90 -0
  40. package/lib/ServerEndpoints.d.ts +133 -0
  41. package/lib/ServerEndpoints.js +247 -0
  42. package/lib/ServerInit.d.ts +28 -0
  43. package/lib/ServerInit.js +113 -0
  44. package/lib/SessionRoom.d.ts +391 -0
  45. package/lib/SessionRoom.js +930 -0
  46. package/lib/TlsCertificates.d.ts +35 -0
  47. package/lib/TlsCertificates.js +130 -0
  48. package/lib/admin/AccountRoutes.d.ts +8 -0
  49. package/lib/admin/AccountRoutes.js +502 -0
  50. package/lib/admin/AdminAccounts.d.ts +329 -0
  51. package/lib/admin/AdminAccounts.js +607 -0
  52. package/lib/admin/AdminApiRouter.d.ts +202 -0
  53. package/lib/admin/AdminApiRouter.js +465 -0
  54. package/lib/admin/AdminApiTypes.d.ts +1782 -0
  55. package/lib/admin/AdminApiTypes.js +4 -0
  56. package/lib/admin/AdminAudit.d.ts +113 -0
  57. package/lib/admin/AdminAudit.js +266 -0
  58. package/lib/admin/AdminCli.d.ts +7 -0
  59. package/lib/admin/AdminCli.js +351 -0
  60. package/lib/admin/AdminClock.d.ts +51 -0
  61. package/lib/admin/AdminClock.js +10 -0
  62. package/lib/admin/AdminConsole.d.ts +39 -0
  63. package/lib/admin/AdminConsole.js +325 -0
  64. package/lib/admin/AdminConstants.d.ts +147 -0
  65. package/lib/admin/AdminConstants.js +167 -0
  66. package/lib/admin/AdminCrypto.d.ts +19 -0
  67. package/lib/admin/AdminCrypto.js +29 -0
  68. package/lib/admin/AdminExtensionsHost.d.ts +176 -0
  69. package/lib/admin/AdminExtensionsHost.js +307 -0
  70. package/lib/admin/AdminPasswords.d.ts +33 -0
  71. package/lib/admin/AdminPasswords.js +92 -0
  72. package/lib/admin/AdminPermissions.d.ts +28 -0
  73. package/lib/admin/AdminPermissions.js +74 -0
  74. package/lib/admin/AdminRecords.d.ts +196 -0
  75. package/lib/admin/AdminRecords.js +3 -0
  76. package/lib/admin/AdminRequestBodies.d.ts +47 -0
  77. package/lib/admin/AdminRequestBodies.js +84 -0
  78. package/lib/admin/AdminRequestGate.d.ts +38 -0
  79. package/lib/admin/AdminRequestGate.js +59 -0
  80. package/lib/admin/AdminResponses.d.ts +103 -0
  81. package/lib/admin/AdminResponses.js +164 -0
  82. package/lib/admin/AdminServices.d.ts +99 -0
  83. package/lib/admin/AdminServices.js +4 -0
  84. package/lib/admin/AdminStaticFiles.d.ts +29 -0
  85. package/lib/admin/AdminStaticFiles.js +97 -0
  86. package/lib/admin/AdminStore.d.ts +152 -0
  87. package/lib/admin/AdminStore.js +46 -0
  88. package/lib/admin/AlertChannels.d.ts +43 -0
  89. package/lib/admin/AlertChannels.js +120 -0
  90. package/lib/admin/AlertEngine.d.ts +231 -0
  91. package/lib/admin/AlertEngine.js +681 -0
  92. package/lib/admin/AlertRoutes.d.ts +8 -0
  93. package/lib/admin/AlertRoutes.js +180 -0
  94. package/lib/admin/ApiKeyRoutes.d.ts +8 -0
  95. package/lib/admin/ApiKeyRoutes.js +53 -0
  96. package/lib/admin/ApiKeys.d.ts +63 -0
  97. package/lib/admin/ApiKeys.js +167 -0
  98. package/lib/admin/AuditRoutes.d.ts +8 -0
  99. package/lib/admin/AuditRoutes.js +106 -0
  100. package/lib/admin/AuthToolRoutes.d.ts +13 -0
  101. package/lib/admin/AuthToolRoutes.js +164 -0
  102. package/lib/admin/BanList.d.ts +87 -0
  103. package/lib/admin/BanList.js +152 -0
  104. package/lib/admin/CborDecoder.d.ts +18 -0
  105. package/lib/admin/CborDecoder.js +201 -0
  106. package/lib/admin/ColyseusCloudClient.d.ts +313 -0
  107. package/lib/admin/ColyseusCloudClient.js +272 -0
  108. package/lib/admin/ColyseusCloudRoutes.d.ts +8 -0
  109. package/lib/admin/ColyseusCloudRoutes.js +91 -0
  110. package/lib/admin/ColyseusCloudService.d.ts +180 -0
  111. package/lib/admin/ColyseusCloudService.js +476 -0
  112. package/lib/admin/ColyseusCloudSettings.d.ts +16 -0
  113. package/lib/admin/ColyseusCloudSettings.js +29 -0
  114. package/lib/admin/ColyseusCloudViews.d.ts +105 -0
  115. package/lib/admin/ColyseusCloudViews.js +371 -0
  116. package/lib/admin/CommonPasswords.d.ts +6 -0
  117. package/lib/admin/CommonPasswords.js +19 -0
  118. package/lib/admin/ConfigOverrides.d.ts +174 -0
  119. package/lib/admin/ConfigOverrides.js +406 -0
  120. package/lib/admin/ConfigRoutes.d.ts +8 -0
  121. package/lib/admin/ConfigRoutes.js +91 -0
  122. package/lib/admin/ConsoleSessions.d.ts +112 -0
  123. package/lib/admin/ConsoleSessions.js +201 -0
  124. package/lib/admin/DedicatedRoutes.d.ts +21 -0
  125. package/lib/admin/DedicatedRoutes.js +254 -0
  126. package/lib/admin/ExtensionRoutes.d.ts +8 -0
  127. package/lib/admin/ExtensionRoutes.js +145 -0
  128. package/lib/admin/FileAdminStore.d.ts +157 -0
  129. package/lib/admin/FileAdminStore.js +349 -0
  130. package/lib/admin/FleetDirectory.d.ts +323 -0
  131. package/lib/admin/FleetDirectory.js +489 -0
  132. package/lib/admin/FleetLeader.d.ts +43 -0
  133. package/lib/admin/FleetLeader.js +89 -0
  134. package/lib/admin/FleetRoutes.d.ts +55 -0
  135. package/lib/admin/FleetRoutes.js +465 -0
  136. package/lib/admin/HealthWarnings.d.ts +10 -0
  137. package/lib/admin/HealthWarnings.js +95 -0
  138. package/lib/admin/LeaderboardRoutes.d.ts +8 -0
  139. package/lib/admin/LeaderboardRoutes.js +569 -0
  140. package/lib/admin/LogBuffer.d.ts +60 -0
  141. package/lib/admin/LogBuffer.js +92 -0
  142. package/lib/admin/LogRedaction.d.ts +15 -0
  143. package/lib/admin/LogRedaction.js +72 -0
  144. package/lib/admin/LogRoutes.d.ts +45 -0
  145. package/lib/admin/LogRoutes.js +307 -0
  146. package/lib/admin/MaintenanceMode.d.ts +41 -0
  147. package/lib/admin/MaintenanceMode.js +74 -0
  148. package/lib/admin/MaintenanceWindows.d.ts +91 -0
  149. package/lib/admin/MaintenanceWindows.js +207 -0
  150. package/lib/admin/MetricsRoutes.d.ts +8 -0
  151. package/lib/admin/MetricsRoutes.js +209 -0
  152. package/lib/admin/MetricsSampler.d.ts +124 -0
  153. package/lib/admin/MetricsSampler.js +211 -0
  154. package/lib/admin/OneTimeCodes.d.ts +60 -0
  155. package/lib/admin/OneTimeCodes.js +162 -0
  156. package/lib/admin/OpenIdConnect.d.ts +146 -0
  157. package/lib/admin/OpenIdConnect.js +335 -0
  158. package/lib/admin/Passkeys.d.ts +130 -0
  159. package/lib/admin/Passkeys.js +432 -0
  160. package/lib/admin/PlayerRoutes.d.ts +20 -0
  161. package/lib/admin/PlayerRoutes.js +676 -0
  162. package/lib/admin/RedisAdminStore.d.ts +114 -0
  163. package/lib/admin/RedisAdminStore.js +258 -0
  164. package/lib/admin/RolloutCoordinator.d.ts +99 -0
  165. package/lib/admin/RolloutCoordinator.js +321 -0
  166. package/lib/admin/ServerEvents.d.ts +57 -0
  167. package/lib/admin/ServerEvents.js +89 -0
  168. package/lib/admin/SessionRoutes.d.ts +113 -0
  169. package/lib/admin/SessionRoutes.js +386 -0
  170. package/lib/admin/UserRoutes.d.ts +8 -0
  171. package/lib/admin/UserRoutes.js +214 -0
  172. package/lib/cli.d.ts +2 -0
  173. package/lib/cli.js +221 -0
  174. package/lib/console/assets/index-CI5N7c-T.js +21 -0
  175. package/lib/console/assets/index-DPitvzLp.css +1 -0
  176. package/lib/console/favicon.svg +1 -0
  177. package/lib/console/index.html +16 -0
  178. package/lib/discord/DiscordOAuth.d.ts +57 -0
  179. package/lib/discord/DiscordOAuth.js +127 -0
  180. package/lib/discord/DiscordSessions.d.ts +79 -0
  181. package/lib/discord/DiscordSessions.js +135 -0
  182. package/lib/discord/DiscordSignInConstants.d.ts +54 -0
  183. package/lib/discord/DiscordSignInConstants.js +55 -0
  184. package/lib/discord/DiscordSignInRoutes.d.ts +51 -0
  185. package/lib/discord/DiscordSignInRoutes.js +359 -0
  186. package/lib/index.d.ts +24 -0
  187. package/lib/index.js +18 -0
  188. package/lib/leaderboards/FileLeaderboardRankStore.d.ts +293 -0
  189. package/lib/leaderboards/FileLeaderboardRankStore.js +808 -0
  190. package/lib/leaderboards/LeaderboardConstants.d.ts +63 -0
  191. package/lib/leaderboards/LeaderboardConstants.js +70 -0
  192. package/lib/leaderboards/LeaderboardError.d.ts +15 -0
  193. package/lib/leaderboards/LeaderboardError.js +20 -0
  194. package/lib/leaderboards/LeaderboardIdentity.d.ts +44 -0
  195. package/lib/leaderboards/LeaderboardIdentity.js +72 -0
  196. package/lib/leaderboards/LeaderboardPublicApi.d.ts +17 -0
  197. package/lib/leaderboards/LeaderboardPublicApi.js +389 -0
  198. package/lib/leaderboards/LeaderboardRankStore.d.ts +296 -0
  199. package/lib/leaderboards/LeaderboardRankStore.js +18 -0
  200. package/lib/leaderboards/LeaderboardRules.d.ts +122 -0
  201. package/lib/leaderboards/LeaderboardRules.js +472 -0
  202. package/lib/leaderboards/LeaderboardScheduler.d.ts +43 -0
  203. package/lib/leaderboards/LeaderboardScheduler.js +103 -0
  204. package/lib/leaderboards/LeaderboardService.d.ts +669 -0
  205. package/lib/leaderboards/LeaderboardService.js +1667 -0
  206. package/lib/leaderboards/LeaderboardTransfer.d.ts +35 -0
  207. package/lib/leaderboards/LeaderboardTransfer.js +580 -0
  208. package/lib/leaderboards/RedisLeaderboardRankStore.d.ts +282 -0
  209. package/lib/leaderboards/RedisLeaderboardRankStore.js +691 -0
  210. package/package.json +64 -4
  211. package/templates/init/Caddyfile +6 -0
  212. package/templates/init/DEPLOY.md +233 -0
  213. package/templates/init/Dockerfile +12 -0
  214. package/templates/init/app.json +21 -0
  215. package/templates/init/aws/README.md +118 -0
  216. package/templates/init/aws/ecs-fargate-alb.yml +207 -0
  217. package/templates/init/aws/ecs-fargate-nlb-quic.yml +210 -0
  218. package/templates/init/babylon-game-server.config.mjs +158 -0
  219. package/templates/init/colyseus-cloud/README.md +133 -0
  220. package/templates/init/colyseus-cloud/babylon-game-server.config.mjs +36 -0
  221. package/templates/init/colyseus-cloud/ecosystem.config.cjs +35 -0
  222. package/templates/init/colyseus-cloud/gitignore +6 -0
  223. package/templates/init/colyseus-cloud/package.json +15 -0
  224. package/templates/init/digitalocean-app.yaml +30 -0
  225. package/templates/init/docker-compose.full.yml +45 -0
  226. package/templates/init/docker-compose.websocket.yml +41 -0
  227. package/templates/init/fly.toml +56 -0
  228. package/templates/init/heroku.yml +11 -0
  229. package/templates/init/kubernetes/deployment.yaml +71 -0
  230. package/templates/init/kubernetes/redis.yaml +73 -0
  231. package/templates/init/kubernetes/service.yaml +24 -0
  232. package/templates/init/monitoring/grafana-dashboard.json +539 -0
  233. package/templates/init/monitoring/prometheus-alerts.yml +51 -0
  234. package/templates/init/monitoring/prometheus-scrape.yml +17 -0
  235. package/templates/init/railway.json +13 -0
  236. package/templates/init/render.yaml +19 -0
@@ -0,0 +1,164 @@
1
+ // The auth tool routes (game-server-admin-console DR §6.11 auth-tools rows): the player auth status with recent
2
+ // failures, a token tester that never logs or counts the token, and a JWKS reload on every instance, plus this
3
+ // instance's reloadAuthKeys fleet command (R15).
4
+ import { testPlayerToken } from "../Authentication.js";
5
+ import { AdminRequestError } from "./AdminApiRouter.js";
6
+ import { bodyObject, optionalStringField, stringField } from "./AdminRequestBodies.js";
7
+ import { canViewAddresses } from "./SessionRoutes.js";
8
+ /** HTTP status of a successful read or action. */
9
+ const HTTP_OK = 200;
10
+ /** HTTP status of an invalid request. */
11
+ const HTTP_BAD_REQUEST = 400;
12
+ /** HTTP status of a tool that does not apply to this server's auth setup. */
13
+ const HTTP_CONFLICT = 409;
14
+ /** The log message of a refused join (JoinGate). */
15
+ const JOIN_REFUSED_MESSAGE = "join refused";
16
+ /** The refusal reasons that are failed token checks. */
17
+ const AUTH_FAILURE_REASONS = new Set(["InvalidAuthentication", "AuthenticationTicketExpired", "CustomAuthenticationFailed"]);
18
+ /** How many join-refused entries each instance is asked for. */
19
+ const FAILURE_ENTRIES_PER_INSTANCE = 1000;
20
+ /** How far back the failure counts look, in milliseconds. */
21
+ const FAILURE_WINDOW_MS = 3600000;
22
+ /** How many failing addresses the status lists. */
23
+ const TOP_ADDRESS_COUNT = 10;
24
+ /** What a secret that has a value shows. */
25
+ const SECRET_SET = "set";
26
+ /** What a secret without a value shows. */
27
+ const SECRET_NOT_SET = "not set";
28
+ /** The token kinds the tester accepts. */
29
+ const TOKEN_KINDS = new Set(["session", "lobby"]);
30
+ /**
31
+ * Registers the auth-tools/* routes.
32
+ * @param router The admin API router.
33
+ * @param services The shared services.
34
+ */
35
+ export function registerAuthToolRoutes(router, services) {
36
+ const routes = [
37
+ { method: "GET", pattern: "auth-tools/status", permission: "auth.view", requiresStepUp: false, auditAction: null, handler: async (request) => ({ status: HTTP_OK, body: await buildAuthStatus(services, request) }) },
38
+ { method: "POST", pattern: "auth-tools/test-token", permission: "auth.view", requiresStepUp: false, auditAction: "auth.testToken", handler: async (request) => testToken(services, request) },
39
+ { method: "POST", pattern: "auth-tools/reload-keys", permission: "auth.manage", requiresStepUp: false, auditAction: "auth.reloadKeys", handler: async () => reloadKeys(services) }
40
+ ];
41
+ for (const route of routes)
42
+ router.register(route);
43
+ }
44
+ /**
45
+ * Registers this instance's reloadAuthKeys fleet command (R15).
46
+ * @param services The shared services.
47
+ */
48
+ export function registerAuthToolCommands(services) {
49
+ services.fleet.handle("reloadAuthKeys", async () => {
50
+ const reload = services.state.verifier?.reloadKeys ?? null;
51
+ if (reload == null)
52
+ throw new AdminRequestError(HTTP_CONFLICT, "this instance does not load player keys from a JWKS URL");
53
+ await reload();
54
+ return null;
55
+ });
56
+ }
57
+ /**
58
+ * GET auth-tools/status (spec 10): the auth setup without secret values, and the failed token checks of the last hour
59
+ * by reason and by address (from the join-refused log entries of every instance).
60
+ * @param services The shared services.
61
+ * @param request The request (addresses only with players.viewAddresses).
62
+ * @returns The status.
63
+ */
64
+ async function buildAuthStatus(services, request) {
65
+ const auth = services.config.auth;
66
+ const failures = await recentAuthFailures(services);
67
+ return {
68
+ enabled: auth.enabled,
69
+ mode: auth.mode,
70
+ issuer: auth.jwt.issuer,
71
+ audience: auth.jwt.audience,
72
+ jwksUrl: auth.jwt.jwksUrl,
73
+ jwksCacheAgeSeconds: services.state.verifier?.keyCacheAgeSeconds() ?? null,
74
+ webhookUrl: (auth.webhook.url != null) ? originOnly(auth.webhook.url) : null,
75
+ webhookTimeoutMs: auth.webhook.timeoutMs,
76
+ sharedSecretTokens: (auth.sharedSecret.tokens.length > 0) ? SECRET_SET : SECRET_NOT_SET,
77
+ jwtSecret: (auth.jwt.secret != null && auth.jwt.secret !== "") ? SECRET_SET : SECRET_NOT_SET,
78
+ failuresLastHour: countBy(failures, (entry) => String(entry.fields.reason)).map((counted) => ({ reason: counted.key, count: counted.count })),
79
+ topFailingAddresses: canViewAddresses(request) ? topAddresses(failures) : []
80
+ };
81
+ }
82
+ /**
83
+ * The join-refused entries of failed token checks in the last hour, across the fleet.
84
+ * @param services The shared services.
85
+ * @returns The entries.
86
+ */
87
+ async function recentAuthFailures(services) {
88
+ const read = await services.fleet.runOnAll("readLogs", { filter: { text: JOIN_REFUSED_MESSAGE }, afterSequence: 0, limit: FAILURE_ENTRIES_PER_INSTANCE });
89
+ const cutoff = services.clock.now() - FAILURE_WINDOW_MS;
90
+ return read.results.flatMap((entry) => entry.result).filter((logEntry) => {
91
+ return logEntry.message === JOIN_REFUSED_MESSAGE && AUTH_FAILURE_REASONS.has(String(logEntry.fields.reason)) && Date.parse(logEntry.time) >= cutoff;
92
+ });
93
+ }
94
+ /**
95
+ * The addresses with the most failed token checks.
96
+ * @param failures The failure entries.
97
+ * @returns At most TOP_ADDRESS_COUNT addresses, most failures first.
98
+ */
99
+ function topAddresses(failures) {
100
+ const withAddress = failures.filter((entry) => typeof entry.fields.clientAddress === "string");
101
+ return countBy(withAddress, (entry) => String(entry.fields.clientAddress)).slice(0, TOP_ADDRESS_COUNT).map((counted) => ({ address: counted.key, count: counted.count }));
102
+ }
103
+ /**
104
+ * Counts entries by a key.
105
+ * @param entries The entries.
106
+ * @param keyOf The key of an entry.
107
+ * @returns Each key with its count, largest count first.
108
+ */
109
+ function countBy(entries, keyOf) {
110
+ const counts = new Map();
111
+ for (const entry of entries)
112
+ counts.set(keyOf(entry), (counts.get(keyOf(entry)) ?? 0) + 1);
113
+ return Array.from(counts.entries()).map(([key, count]) => ({ key: key, count: count })).sort((left, right) => right.count - left.count);
114
+ }
115
+ /**
116
+ * A URL shown as scheme://host/… only, so a path or query that carries a secret never shows.
117
+ * @param url The URL.
118
+ * @returns The shortened URL, or "(invalid URL)".
119
+ */
120
+ function originOnly(url) {
121
+ try {
122
+ const parsed = new URL(url);
123
+ return parsed.protocol + "//" + parsed.host + "/…";
124
+ }
125
+ catch {
126
+ return "(invalid URL)";
127
+ }
128
+ }
129
+ /**
130
+ * POST auth-tools/test-token: checks a token the way the join gate would, without counting it or logging it. The
131
+ * audit record never holds the token.
132
+ * @param services The shared services.
133
+ * @param request The request.
134
+ * @returns The outcome.
135
+ * @throws AdminRequestError 400 for a bad body, 409 while auth is off.
136
+ */
137
+ async function testToken(services, request) {
138
+ const body = bodyObject(request.body);
139
+ const token = stringField(body, "token");
140
+ const kind = stringField(body, "kind");
141
+ if (!TOKEN_KINDS.has(kind))
142
+ throw new AdminRequestError(HTTP_BAD_REQUEST, "kind must be session or lobby");
143
+ const sessionName = optionalStringField(body, "sessionName");
144
+ if (services.state.verifier == null)
145
+ throw new AdminRequestError(HTTP_CONFLICT, "player auth is off, so joins need no token");
146
+ const authRequest = { token: token, kind: kind, sessionName: (kind === "session") ? sessionName : null, gameMode: null, clientAddress: request.clientAddress, origin: null };
147
+ const result = await testPlayerToken(services.state.verifier, authRequest);
148
+ const view = { accepted: result.accepted, identity: (result.identity != null) ? { userId: result.identity.userId, claims: result.identity.claims } : null, refusal: result.refusal };
149
+ return { status: HTTP_OK, body: view, audit: { target: kind, parameters: { sessionName: sessionName, accepted: result.accepted, refusal: result.refusal } } };
150
+ }
151
+ /**
152
+ * POST auth-tools/reload-keys: every instance fetches the JWKS again now.
153
+ * @param services The shared services.
154
+ * @returns How many instances reloaded, and each failure.
155
+ * @throws AdminRequestError 409 when player auth does not use a JWKS URL.
156
+ */
157
+ async function reloadKeys(services) {
158
+ const auth = services.config.auth;
159
+ if (!auth.enabled || auth.mode !== "jwt" || auth.jwt.jwksUrl == null)
160
+ throw new AdminRequestError(HTTP_CONFLICT, "player auth does not load keys from a JWKS URL");
161
+ const outcome = await services.fleet.runOnAll("reloadAuthKeys", null);
162
+ const view = { reloadedInstances: outcome.results.length, failures: outcome.failures.map((failure) => failure.processId + ": " + failure.error) };
163
+ return { status: HTTP_OK, body: view, audit: { target: "fleet", parameters: { reloadedInstances: view.reloadedInstances, failures: view.failures.length } } };
164
+ }
@@ -0,0 +1,87 @@
1
+ import { type Presence } from "colyseus";
2
+ import type { BanRecord } from "./AdminRecords.js";
3
+ /** Who a kick block refuses: the user id when the player had one, else its address (D12). */
4
+ export interface KickSubject {
5
+ /** The kicked player's user id, or null when it joined anonymously. */
6
+ userId: string | null;
7
+ /** The kicked player's address. */
8
+ clientAddress: string;
9
+ }
10
+ /**
11
+ * Whether a ban is in force at a time.
12
+ * @param record The ban.
13
+ * @param nowMilliseconds The time.
14
+ * @returns True for a permanent ban or one that ends later.
15
+ */
16
+ export declare function isBanActive(record: BanRecord, nowMilliseconds: number): boolean;
17
+ /**
18
+ * The refusal detail of a ban (D15).
19
+ * @param record The ban.
20
+ * @returns "you are banned until <ISO time>" or "you are banned permanently".
21
+ */
22
+ export declare function banRefusalDetail(record: BanRecord): string;
23
+ /** The bans of this instance, as the join gate checks them. */
24
+ export declare class BanList {
25
+ /** The current time, in milliseconds (the admin clock). */
26
+ private readonly now;
27
+ /** Address and network bans. */
28
+ private addressBans;
29
+ /** User-id bans by user id. */
30
+ private userBans;
31
+ /**
32
+ * Starts empty; reload() fills it.
33
+ * @param now The current time, in milliseconds.
34
+ */
35
+ constructor(now: () => number);
36
+ /**
37
+ * Replaces the bans; records already expired are dropped and unreadable ranges are skipped.
38
+ * @param records Every stored ban.
39
+ */
40
+ reload(records: BanRecord[]): void;
41
+ /**
42
+ * The ban in force on an address, by address or by range.
43
+ * @param clientAddress The client's address.
44
+ * @returns The ban, or null.
45
+ */
46
+ addressBan(clientAddress: string): BanRecord | null;
47
+ /**
48
+ * The ban in force on a user id.
49
+ * @param userId The verified user id.
50
+ * @returns The ban, or null.
51
+ */
52
+ userBan(userId: string): BanRecord | null;
53
+ /**
54
+ * The ban in force on a player, by user id or by address.
55
+ * @param userId The player's user id, or null.
56
+ * @param clientAddress The player's address.
57
+ * @returns The ban, or null.
58
+ */
59
+ playerBan(userId: string | null, clientAddress: string): BanRecord | null;
60
+ }
61
+ /** Recently kicked players per session name, kept in presence for KICK_REJOIN_BLOCK_SECONDS (D12, R19). */
62
+ export declare class KickBlockList {
63
+ /** The fleet's presence (read on each call: Colyseus sets it when the server listens). */
64
+ private readonly presence;
65
+ /**
66
+ * Creates the list.
67
+ * @param presence Returns the fleet's presence.
68
+ */
69
+ constructor(presence: () => Presence);
70
+ /**
71
+ * Refuses a kicked player's fresh joins to one session for a while: by user id, or by address when anonymous. The
72
+ * value is the block's end time, so a read can tell an expired block. Redis expires the key itself; local presence
73
+ * gets a plain key, because its expiry timer would keep a closed server's process alive for the whole block.
74
+ * @param sessionName The session it was kicked from.
75
+ * @param subject Who was kicked.
76
+ * @param seconds How long the block lasts.
77
+ */
78
+ block(sessionName: string, subject: KickSubject, seconds: number): Promise<void>;
79
+ /**
80
+ * Whether a join to a session is blocked by a kick of its user id or of its address.
81
+ * @param sessionName The session.
82
+ * @param userId The joiner's verified user id, or null.
83
+ * @param clientAddress The joiner's address.
84
+ * @returns True when blocked.
85
+ */
86
+ isBlocked(sessionName: string, userId: string | null, clientAddress: string): Promise<boolean>;
87
+ }
@@ -0,0 +1,152 @@
1
+ // Bans and kick blocks the join gate enforces (game-server-admin-console D12, D15, DR §6.4, §13 R19). The BanList is
2
+ // rebuilt from the store's "bans" collection on start and on every change announcement; kick blocks live in presence,
3
+ // so a kicked player is refused on every instance.
4
+ import { LocalPresence } from "colyseus";
5
+ import { BlockList } from "node:net";
6
+ import { addNetworkEntry, isAddressInList, normalizeAddress } from "../NetworkAddresses.js";
7
+ /** Presence key prefix of kick blocks (R19). */
8
+ const KICK_BLOCK_KEY_PREFIX = "bgs:kick:";
9
+ /** Milliseconds per second. */
10
+ const MILLISECONDS_PER_SECOND = 1000;
11
+ /**
12
+ * Whether a ban is in force at a time.
13
+ * @param record The ban.
14
+ * @param nowMilliseconds The time.
15
+ * @returns True for a permanent ban or one that ends later.
16
+ */
17
+ export function isBanActive(record, nowMilliseconds) {
18
+ return record.expiresAt == null || Date.parse(record.expiresAt) > nowMilliseconds;
19
+ }
20
+ /**
21
+ * The refusal detail of a ban (D15).
22
+ * @param record The ban.
23
+ * @returns "you are banned until <ISO time>" or "you are banned permanently".
24
+ */
25
+ export function banRefusalDetail(record) {
26
+ return (record.expiresAt != null) ? "you are banned until " + record.expiresAt : "you are banned permanently";
27
+ }
28
+ /** The bans of this instance, as the join gate checks them. */
29
+ export class BanList {
30
+ /** The current time, in milliseconds (the admin clock). */
31
+ now;
32
+ /** Address and network bans. */
33
+ addressBans = [];
34
+ /** User-id bans by user id. */
35
+ userBans = new Map();
36
+ /**
37
+ * Starts empty; reload() fills it.
38
+ * @param now The current time, in milliseconds.
39
+ */
40
+ constructor(now) {
41
+ this.now = now;
42
+ }
43
+ /**
44
+ * Replaces the bans; records already expired are dropped and unreadable ranges are skipped.
45
+ * @param records Every stored ban.
46
+ */
47
+ reload(records) {
48
+ const now = this.now();
49
+ const active = records.filter((record) => isBanActive(record, now));
50
+ this.userBans = new Map(active.filter((record) => record.kind === "user").map((record) => [record.value, record]));
51
+ this.addressBans = active.filter((record) => record.kind !== "user").map(createAddressEntry).filter((entry) => entry != null);
52
+ }
53
+ /**
54
+ * The ban in force on an address, by address or by range.
55
+ * @param clientAddress The client's address.
56
+ * @returns The ban, or null.
57
+ */
58
+ addressBan(clientAddress) {
59
+ const now = this.now();
60
+ const entry = this.addressBans.find((candidate) => isBanActive(candidate.record, now) && isAddressInList(candidate.matcher, clientAddress));
61
+ return entry?.record ?? null;
62
+ }
63
+ /**
64
+ * The ban in force on a user id.
65
+ * @param userId The verified user id.
66
+ * @returns The ban, or null.
67
+ */
68
+ userBan(userId) {
69
+ const record = this.userBans.get(userId);
70
+ return (record != null && isBanActive(record, this.now())) ? record : null;
71
+ }
72
+ /**
73
+ * The ban in force on a player, by user id or by address.
74
+ * @param userId The player's user id, or null.
75
+ * @param clientAddress The player's address.
76
+ * @returns The ban, or null.
77
+ */
78
+ playerBan(userId, clientAddress) {
79
+ return ((userId != null) ? this.userBan(userId) : null) ?? this.addressBan(clientAddress);
80
+ }
81
+ }
82
+ /**
83
+ * The block list entry of an address or network ban.
84
+ * @param record The ban.
85
+ * @returns The entry, or null when the value is not an address or range.
86
+ */
87
+ function createAddressEntry(record) {
88
+ const matcher = new BlockList();
89
+ return addNetworkEntry(matcher, record.value) ? { record: record, matcher: matcher } : null;
90
+ }
91
+ /** Recently kicked players per session name, kept in presence for KICK_REJOIN_BLOCK_SECONDS (D12, R19). */
92
+ export class KickBlockList {
93
+ /** The fleet's presence (read on each call: Colyseus sets it when the server listens). */
94
+ presence;
95
+ /**
96
+ * Creates the list.
97
+ * @param presence Returns the fleet's presence.
98
+ */
99
+ constructor(presence) {
100
+ this.presence = presence;
101
+ }
102
+ /**
103
+ * Refuses a kicked player's fresh joins to one session for a while: by user id, or by address when anonymous. The
104
+ * value is the block's end time, so a read can tell an expired block. Redis expires the key itself; local presence
105
+ * gets a plain key, because its expiry timer would keep a closed server's process alive for the whole block.
106
+ * @param sessionName The session it was kicked from.
107
+ * @param subject Who was kicked.
108
+ * @param seconds How long the block lasts.
109
+ */
110
+ async block(sessionName, subject, seconds) {
111
+ const key = (subject.userId != null) ? userKey(sessionName, subject.userId) : addressKey(sessionName, subject.clientAddress);
112
+ const endsAt = String(Date.now() + seconds * MILLISECONDS_PER_SECOND);
113
+ const presence = this.presence();
114
+ if (presence instanceof LocalPresence)
115
+ await presence.set(key, endsAt);
116
+ else
117
+ await presence.setex(key, endsAt, seconds);
118
+ }
119
+ /**
120
+ * Whether a join to a session is blocked by a kick of its user id or of its address.
121
+ * @param sessionName The session.
122
+ * @param userId The joiner's verified user id, or null.
123
+ * @param clientAddress The joiner's address.
124
+ * @returns True when blocked.
125
+ */
126
+ async isBlocked(sessionName, userId, clientAddress) {
127
+ const keys = [addressKey(sessionName, clientAddress)];
128
+ if (userId != null)
129
+ keys.push(userKey(sessionName, userId));
130
+ const values = await Promise.all(keys.map((key) => this.presence().get(key)));
131
+ const now = Date.now();
132
+ return values.some((value) => value != null && Number(value) > now);
133
+ }
134
+ }
135
+ /**
136
+ * The presence key of a user-id kick block.
137
+ * @param sessionName The session.
138
+ * @param userId The user id.
139
+ * @returns The key.
140
+ */
141
+ function userKey(sessionName, userId) {
142
+ return KICK_BLOCK_KEY_PREFIX + sessionName + ":user:" + userId;
143
+ }
144
+ /**
145
+ * The presence key of an address kick block.
146
+ * @param sessionName The session.
147
+ * @param clientAddress The address (normalized, so IPv4-mapped forms match).
148
+ * @returns The key.
149
+ */
150
+ function addressKey(sessionName, clientAddress) {
151
+ return KICK_BLOCK_KEY_PREFIX + sessionName + ":address:" + normalizeAddress(clientAddress);
152
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Decodes exactly one CBOR item.
3
+ * @param bytes The encoded item.
4
+ * @returns The value: numbers, Uint8Array byte strings, strings, arrays, Map objects, booleans or null.
5
+ * @throws Error "unsupported CBOR" for unsupported types, indefinite lengths, truncated input or trailing bytes.
6
+ */
7
+ export declare function decodeCbor(bytes: Uint8Array): unknown;
8
+ /**
9
+ * Decodes the first CBOR item of some bytes and says where it ends (the credential public key inside authenticator
10
+ * data is followed by extension data).
11
+ * @param bytes The bytes, starting with an item.
12
+ * @returns The value and the number of bytes it took.
13
+ * @throws Error "unsupported CBOR" for an unsupported or truncated item.
14
+ */
15
+ export declare function decodeCborPrefix(bytes: Uint8Array): {
16
+ value: unknown;
17
+ byteLength: number;
18
+ };
@@ -0,0 +1,201 @@
1
+ // A minimal CBOR decoder (RFC 8949) for passkey attestation objects and COSE keys (game-server-admin-console D31): major
2
+ // types 0–5 and the simple values false, true and null of major type 7, definite lengths only. Maps become Map objects
3
+ // so integer keys (COSE labels) keep their type. Anything else is refused.
4
+ /** Major types of the initial byte's top three bits. */
5
+ const MAJOR_UNSIGNED = 0;
6
+ const MAJOR_NEGATIVE = 1;
7
+ const MAJOR_BYTES = 2;
8
+ const MAJOR_TEXT = 3;
9
+ const MAJOR_ARRAY = 4;
10
+ const MAJOR_MAP = 5;
11
+ const MAJOR_SIMPLE = 7;
12
+ /** Additional information values: 0–23 are the value itself; 24–27 a following 1, 2, 4 or 8 byte argument. */
13
+ const DIRECT_ARGUMENT_MAX = 23;
14
+ const ONE_BYTE_ARGUMENT = 24;
15
+ const TWO_BYTE_ARGUMENT = 25;
16
+ const FOUR_BYTE_ARGUMENT = 26;
17
+ const EIGHT_BYTE_ARGUMENT = 27;
18
+ /** Simple values of major type 7. */
19
+ const SIMPLE_FALSE = 20;
20
+ const SIMPLE_TRUE = 21;
21
+ const SIMPLE_NULL = 22;
22
+ /** Bits of the major type and the additional information in the initial byte. */
23
+ const MAJOR_TYPE_SHIFT = 5;
24
+ const ADDITIONAL_INFORMATION_MASK = 0x1f;
25
+ /** The error every unsupported or malformed input throws. */
26
+ const UNSUPPORTED_MESSAGE = "unsupported CBOR";
27
+ /** Reads one CBOR item after another from a byte array. */
28
+ class CborReader {
29
+ /** The input. */
30
+ bytes;
31
+ /** A view of the input for multi-byte arguments. */
32
+ view;
33
+ /** The next byte to read. */
34
+ offset = 0;
35
+ /**
36
+ * Starts at the first byte.
37
+ * @param bytes The input.
38
+ */
39
+ constructor(bytes) {
40
+ this.bytes = bytes;
41
+ this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
42
+ }
43
+ /** True once every byte was read. */
44
+ get isAtEnd() {
45
+ return this.offset === this.bytes.byteLength;
46
+ }
47
+ /** How many bytes were read so far. */
48
+ get bytesRead() {
49
+ return this.offset;
50
+ }
51
+ /**
52
+ * Reads one item.
53
+ * @returns The decoded value.
54
+ * @throws Error "unsupported CBOR" for an unsupported or truncated item.
55
+ */
56
+ readItem() {
57
+ const initialByte = this.readUint8();
58
+ const majorType = initialByte >> MAJOR_TYPE_SHIFT;
59
+ const additionalInformation = initialByte & ADDITIONAL_INFORMATION_MASK;
60
+ if (majorType === MAJOR_SIMPLE)
61
+ return simpleValueOf(additionalInformation);
62
+ const argument = this.readArgument(additionalInformation);
63
+ switch (majorType) {
64
+ case MAJOR_UNSIGNED:
65
+ return argument;
66
+ case MAJOR_NEGATIVE:
67
+ return -1 - argument;
68
+ case MAJOR_BYTES:
69
+ return this.readBytes(argument).slice();
70
+ case MAJOR_TEXT:
71
+ return new TextDecoder("utf-8", { fatal: true }).decode(this.readBytes(argument));
72
+ case MAJOR_ARRAY:
73
+ return this.readArray(argument);
74
+ case MAJOR_MAP:
75
+ return this.readMap(argument);
76
+ default:
77
+ throw new Error(UNSUPPORTED_MESSAGE);
78
+ }
79
+ }
80
+ /**
81
+ * Reads the argument an item's additional information announces.
82
+ * @param additionalInformation The low five bits of the initial byte.
83
+ * @returns The argument (a length or an integer).
84
+ * @throws Error "unsupported CBOR" for indefinite lengths, reserved values or integers above 2^53 − 1.
85
+ */
86
+ readArgument(additionalInformation) {
87
+ if (additionalInformation <= DIRECT_ARGUMENT_MAX)
88
+ return additionalInformation;
89
+ if (additionalInformation === ONE_BYTE_ARGUMENT)
90
+ return this.readUint8();
91
+ if (additionalInformation === TWO_BYTE_ARGUMENT)
92
+ return this.readFixed(2, () => this.view.getUint16(this.offset));
93
+ if (additionalInformation === FOUR_BYTE_ARGUMENT)
94
+ return this.readFixed(4, () => this.view.getUint32(this.offset));
95
+ if (additionalInformation !== EIGHT_BYTE_ARGUMENT)
96
+ throw new Error(UNSUPPORTED_MESSAGE);
97
+ const wide = this.readFixed(8, () => this.view.getBigUint64(this.offset));
98
+ if (wide > BigInt(Number.MAX_SAFE_INTEGER))
99
+ throw new Error(UNSUPPORTED_MESSAGE);
100
+ return Number(wide);
101
+ }
102
+ /**
103
+ * Reads a definite-length array.
104
+ * @param length The number of items.
105
+ * @returns The items.
106
+ */
107
+ readArray(length) {
108
+ const items = [];
109
+ for (let i = 0; i < length; i++)
110
+ items.push(this.readItem());
111
+ return items;
112
+ }
113
+ /**
114
+ * Reads a definite-length map.
115
+ * @param length The number of pairs.
116
+ * @returns The pairs, keys of any type.
117
+ */
118
+ readMap(length) {
119
+ const map = new Map();
120
+ for (let i = 0; i < length; i++) {
121
+ const key = this.readItem();
122
+ map.set(key, this.readItem());
123
+ }
124
+ return map;
125
+ }
126
+ /**
127
+ * Reads one byte.
128
+ * @returns The byte.
129
+ * @throws Error "unsupported CBOR" past the end.
130
+ */
131
+ readUint8() {
132
+ return this.readFixed(1, () => this.view.getUint8(this.offset));
133
+ }
134
+ /**
135
+ * Reads a fixed-size big-endian value and moves past it.
136
+ * @param size Its size in bytes.
137
+ * @param read Reads it at the current offset.
138
+ * @returns The value.
139
+ * @throws Error "unsupported CBOR" when the input is too short.
140
+ */
141
+ readFixed(size, read) {
142
+ if (this.offset + size > this.bytes.byteLength)
143
+ throw new Error(UNSUPPORTED_MESSAGE);
144
+ const value = read();
145
+ this.offset += size;
146
+ return value;
147
+ }
148
+ /**
149
+ * Reads some bytes and moves past them.
150
+ * @param length How many.
151
+ * @returns A view of them.
152
+ * @throws Error "unsupported CBOR" when the input is too short.
153
+ */
154
+ readBytes(length) {
155
+ if (this.offset + length > this.bytes.byteLength)
156
+ throw new Error(UNSUPPORTED_MESSAGE);
157
+ const slice = this.bytes.subarray(this.offset, this.offset + length);
158
+ this.offset += length;
159
+ return slice;
160
+ }
161
+ }
162
+ /**
163
+ * The simple value of a major type 7 item.
164
+ * @param additionalInformation The low five bits.
165
+ * @returns false, true or null.
166
+ * @throws Error "unsupported CBOR" for floats, undefined and other simple values.
167
+ */
168
+ function simpleValueOf(additionalInformation) {
169
+ if (additionalInformation === SIMPLE_FALSE)
170
+ return false;
171
+ if (additionalInformation === SIMPLE_TRUE)
172
+ return true;
173
+ if (additionalInformation === SIMPLE_NULL)
174
+ return null;
175
+ throw new Error(UNSUPPORTED_MESSAGE);
176
+ }
177
+ /**
178
+ * Decodes exactly one CBOR item.
179
+ * @param bytes The encoded item.
180
+ * @returns The value: numbers, Uint8Array byte strings, strings, arrays, Map objects, booleans or null.
181
+ * @throws Error "unsupported CBOR" for unsupported types, indefinite lengths, truncated input or trailing bytes.
182
+ */
183
+ export function decodeCbor(bytes) {
184
+ const reader = new CborReader(bytes);
185
+ const value = reader.readItem();
186
+ if (!reader.isAtEnd)
187
+ throw new Error(UNSUPPORTED_MESSAGE);
188
+ return value;
189
+ }
190
+ /**
191
+ * Decodes the first CBOR item of some bytes and says where it ends (the credential public key inside authenticator
192
+ * data is followed by extension data).
193
+ * @param bytes The bytes, starting with an item.
194
+ * @returns The value and the number of bytes it took.
195
+ * @throws Error "unsupported CBOR" for an unsupported or truncated item.
196
+ */
197
+ export function decodeCborPrefix(bytes) {
198
+ const reader = new CborReader(bytes);
199
+ const value = reader.readItem();
200
+ return { value: value, byteLength: reader.bytesRead };
201
+ }