@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.
- package/CUSTOM.md +1690 -0
- package/DEPLOY.md +2012 -0
- package/README.md +1081 -2
- package/lib/AbuseLimits.d.ts +64 -0
- package/lib/AbuseLimits.js +121 -0
- package/lib/Authentication.d.ts +86 -0
- package/lib/Authentication.js +272 -0
- package/lib/BootSessionOwner.d.ts +86 -0
- package/lib/BootSessionOwner.js +116 -0
- package/lib/ColyseusLoggerAdapter.d.ts +32 -0
- package/lib/ColyseusLoggerAdapter.js +73 -0
- package/lib/DedicatedSceneRegistry.d.ts +52 -0
- package/lib/DedicatedSceneRegistry.js +74 -0
- package/lib/DedicatedSession.d.ts +152 -0
- package/lib/DedicatedSession.js +173 -0
- package/lib/DualTransport.d.ts +112 -0
- package/lib/DualTransport.js +197 -0
- package/lib/GameServer.d.ts +64 -0
- package/lib/GameServer.js +647 -0
- package/lib/GameServerConfig.d.ts +387 -0
- package/lib/GameServerConfig.js +1059 -0
- package/lib/GameServerLobbyRoom.d.ts +33 -0
- package/lib/GameServerLobbyRoom.js +56 -0
- package/lib/GameServerLogger.d.ts +111 -0
- package/lib/GameServerLogger.js +190 -0
- package/lib/GameServerMetrics.d.ts +158 -0
- package/lib/GameServerMetrics.js +276 -0
- package/lib/GameServerState.d.ts +164 -0
- package/lib/GameServerState.js +26 -0
- package/lib/HeadlessRuntime.d.ts +316 -0
- package/lib/HeadlessRuntime.js +380 -0
- package/lib/JoinGate.d.ts +34 -0
- package/lib/JoinGate.js +221 -0
- package/lib/NetworkAddresses.d.ts +33 -0
- package/lib/NetworkAddresses.js +90 -0
- package/lib/PlayerTokens.d.ts +17 -0
- package/lib/PlayerTokens.js +24 -0
- package/lib/RoomServerTransport.d.ts +73 -0
- package/lib/RoomServerTransport.js +90 -0
- package/lib/ServerEndpoints.d.ts +133 -0
- package/lib/ServerEndpoints.js +247 -0
- package/lib/ServerInit.d.ts +28 -0
- package/lib/ServerInit.js +113 -0
- package/lib/SessionRoom.d.ts +391 -0
- package/lib/SessionRoom.js +930 -0
- package/lib/TlsCertificates.d.ts +35 -0
- package/lib/TlsCertificates.js +130 -0
- package/lib/admin/AccountRoutes.d.ts +8 -0
- package/lib/admin/AccountRoutes.js +502 -0
- package/lib/admin/AdminAccounts.d.ts +329 -0
- package/lib/admin/AdminAccounts.js +607 -0
- package/lib/admin/AdminApiRouter.d.ts +202 -0
- package/lib/admin/AdminApiRouter.js +465 -0
- package/lib/admin/AdminApiTypes.d.ts +1782 -0
- package/lib/admin/AdminApiTypes.js +4 -0
- package/lib/admin/AdminAudit.d.ts +113 -0
- package/lib/admin/AdminAudit.js +266 -0
- package/lib/admin/AdminCli.d.ts +7 -0
- package/lib/admin/AdminCli.js +351 -0
- package/lib/admin/AdminClock.d.ts +51 -0
- package/lib/admin/AdminClock.js +10 -0
- package/lib/admin/AdminConsole.d.ts +39 -0
- package/lib/admin/AdminConsole.js +325 -0
- package/lib/admin/AdminConstants.d.ts +147 -0
- package/lib/admin/AdminConstants.js +167 -0
- package/lib/admin/AdminCrypto.d.ts +19 -0
- package/lib/admin/AdminCrypto.js +29 -0
- package/lib/admin/AdminExtensionsHost.d.ts +176 -0
- package/lib/admin/AdminExtensionsHost.js +307 -0
- package/lib/admin/AdminPasswords.d.ts +33 -0
- package/lib/admin/AdminPasswords.js +92 -0
- package/lib/admin/AdminPermissions.d.ts +28 -0
- package/lib/admin/AdminPermissions.js +74 -0
- package/lib/admin/AdminRecords.d.ts +196 -0
- package/lib/admin/AdminRecords.js +3 -0
- package/lib/admin/AdminRequestBodies.d.ts +47 -0
- package/lib/admin/AdminRequestBodies.js +84 -0
- package/lib/admin/AdminRequestGate.d.ts +38 -0
- package/lib/admin/AdminRequestGate.js +59 -0
- package/lib/admin/AdminResponses.d.ts +103 -0
- package/lib/admin/AdminResponses.js +164 -0
- package/lib/admin/AdminServices.d.ts +99 -0
- package/lib/admin/AdminServices.js +4 -0
- package/lib/admin/AdminStaticFiles.d.ts +29 -0
- package/lib/admin/AdminStaticFiles.js +97 -0
- package/lib/admin/AdminStore.d.ts +152 -0
- package/lib/admin/AdminStore.js +46 -0
- package/lib/admin/AlertChannels.d.ts +43 -0
- package/lib/admin/AlertChannels.js +120 -0
- package/lib/admin/AlertEngine.d.ts +231 -0
- package/lib/admin/AlertEngine.js +681 -0
- package/lib/admin/AlertRoutes.d.ts +8 -0
- package/lib/admin/AlertRoutes.js +180 -0
- package/lib/admin/ApiKeyRoutes.d.ts +8 -0
- package/lib/admin/ApiKeyRoutes.js +53 -0
- package/lib/admin/ApiKeys.d.ts +63 -0
- package/lib/admin/ApiKeys.js +167 -0
- package/lib/admin/AuditRoutes.d.ts +8 -0
- package/lib/admin/AuditRoutes.js +106 -0
- package/lib/admin/AuthToolRoutes.d.ts +13 -0
- package/lib/admin/AuthToolRoutes.js +164 -0
- package/lib/admin/BanList.d.ts +87 -0
- package/lib/admin/BanList.js +152 -0
- package/lib/admin/CborDecoder.d.ts +18 -0
- package/lib/admin/CborDecoder.js +201 -0
- package/lib/admin/ColyseusCloudClient.d.ts +313 -0
- package/lib/admin/ColyseusCloudClient.js +272 -0
- package/lib/admin/ColyseusCloudRoutes.d.ts +8 -0
- package/lib/admin/ColyseusCloudRoutes.js +91 -0
- package/lib/admin/ColyseusCloudService.d.ts +180 -0
- package/lib/admin/ColyseusCloudService.js +476 -0
- package/lib/admin/ColyseusCloudSettings.d.ts +16 -0
- package/lib/admin/ColyseusCloudSettings.js +29 -0
- package/lib/admin/ColyseusCloudViews.d.ts +105 -0
- package/lib/admin/ColyseusCloudViews.js +371 -0
- package/lib/admin/CommonPasswords.d.ts +6 -0
- package/lib/admin/CommonPasswords.js +19 -0
- package/lib/admin/ConfigOverrides.d.ts +174 -0
- package/lib/admin/ConfigOverrides.js +406 -0
- package/lib/admin/ConfigRoutes.d.ts +8 -0
- package/lib/admin/ConfigRoutes.js +91 -0
- package/lib/admin/ConsoleSessions.d.ts +112 -0
- package/lib/admin/ConsoleSessions.js +201 -0
- package/lib/admin/DedicatedRoutes.d.ts +21 -0
- package/lib/admin/DedicatedRoutes.js +254 -0
- package/lib/admin/ExtensionRoutes.d.ts +8 -0
- package/lib/admin/ExtensionRoutes.js +145 -0
- package/lib/admin/FileAdminStore.d.ts +157 -0
- package/lib/admin/FileAdminStore.js +349 -0
- package/lib/admin/FleetDirectory.d.ts +323 -0
- package/lib/admin/FleetDirectory.js +489 -0
- package/lib/admin/FleetLeader.d.ts +43 -0
- package/lib/admin/FleetLeader.js +89 -0
- package/lib/admin/FleetRoutes.d.ts +55 -0
- package/lib/admin/FleetRoutes.js +465 -0
- package/lib/admin/HealthWarnings.d.ts +10 -0
- package/lib/admin/HealthWarnings.js +95 -0
- package/lib/admin/LeaderboardRoutes.d.ts +8 -0
- package/lib/admin/LeaderboardRoutes.js +569 -0
- package/lib/admin/LogBuffer.d.ts +60 -0
- package/lib/admin/LogBuffer.js +92 -0
- package/lib/admin/LogRedaction.d.ts +15 -0
- package/lib/admin/LogRedaction.js +72 -0
- package/lib/admin/LogRoutes.d.ts +45 -0
- package/lib/admin/LogRoutes.js +307 -0
- package/lib/admin/MaintenanceMode.d.ts +41 -0
- package/lib/admin/MaintenanceMode.js +74 -0
- package/lib/admin/MaintenanceWindows.d.ts +91 -0
- package/lib/admin/MaintenanceWindows.js +207 -0
- package/lib/admin/MetricsRoutes.d.ts +8 -0
- package/lib/admin/MetricsRoutes.js +209 -0
- package/lib/admin/MetricsSampler.d.ts +124 -0
- package/lib/admin/MetricsSampler.js +211 -0
- package/lib/admin/OneTimeCodes.d.ts +60 -0
- package/lib/admin/OneTimeCodes.js +162 -0
- package/lib/admin/OpenIdConnect.d.ts +146 -0
- package/lib/admin/OpenIdConnect.js +335 -0
- package/lib/admin/Passkeys.d.ts +130 -0
- package/lib/admin/Passkeys.js +432 -0
- package/lib/admin/PlayerRoutes.d.ts +20 -0
- package/lib/admin/PlayerRoutes.js +676 -0
- package/lib/admin/RedisAdminStore.d.ts +114 -0
- package/lib/admin/RedisAdminStore.js +258 -0
- package/lib/admin/RolloutCoordinator.d.ts +99 -0
- package/lib/admin/RolloutCoordinator.js +321 -0
- package/lib/admin/ServerEvents.d.ts +57 -0
- package/lib/admin/ServerEvents.js +89 -0
- package/lib/admin/SessionRoutes.d.ts +113 -0
- package/lib/admin/SessionRoutes.js +386 -0
- package/lib/admin/UserRoutes.d.ts +8 -0
- package/lib/admin/UserRoutes.js +214 -0
- package/lib/cli.d.ts +2 -0
- package/lib/cli.js +221 -0
- package/lib/console/assets/index-CI5N7c-T.js +21 -0
- package/lib/console/assets/index-DPitvzLp.css +1 -0
- package/lib/console/favicon.svg +1 -0
- package/lib/console/index.html +16 -0
- package/lib/discord/DiscordOAuth.d.ts +57 -0
- package/lib/discord/DiscordOAuth.js +127 -0
- package/lib/discord/DiscordSessions.d.ts +79 -0
- package/lib/discord/DiscordSessions.js +135 -0
- package/lib/discord/DiscordSignInConstants.d.ts +54 -0
- package/lib/discord/DiscordSignInConstants.js +55 -0
- package/lib/discord/DiscordSignInRoutes.d.ts +51 -0
- package/lib/discord/DiscordSignInRoutes.js +359 -0
- package/lib/index.d.ts +24 -0
- package/lib/index.js +18 -0
- package/lib/leaderboards/FileLeaderboardRankStore.d.ts +293 -0
- package/lib/leaderboards/FileLeaderboardRankStore.js +808 -0
- package/lib/leaderboards/LeaderboardConstants.d.ts +63 -0
- package/lib/leaderboards/LeaderboardConstants.js +70 -0
- package/lib/leaderboards/LeaderboardError.d.ts +15 -0
- package/lib/leaderboards/LeaderboardError.js +20 -0
- package/lib/leaderboards/LeaderboardIdentity.d.ts +44 -0
- package/lib/leaderboards/LeaderboardIdentity.js +72 -0
- package/lib/leaderboards/LeaderboardPublicApi.d.ts +17 -0
- package/lib/leaderboards/LeaderboardPublicApi.js +389 -0
- package/lib/leaderboards/LeaderboardRankStore.d.ts +296 -0
- package/lib/leaderboards/LeaderboardRankStore.js +18 -0
- package/lib/leaderboards/LeaderboardRules.d.ts +122 -0
- package/lib/leaderboards/LeaderboardRules.js +472 -0
- package/lib/leaderboards/LeaderboardScheduler.d.ts +43 -0
- package/lib/leaderboards/LeaderboardScheduler.js +103 -0
- package/lib/leaderboards/LeaderboardService.d.ts +669 -0
- package/lib/leaderboards/LeaderboardService.js +1667 -0
- package/lib/leaderboards/LeaderboardTransfer.d.ts +35 -0
- package/lib/leaderboards/LeaderboardTransfer.js +580 -0
- package/lib/leaderboards/RedisLeaderboardRankStore.d.ts +282 -0
- package/lib/leaderboards/RedisLeaderboardRankStore.js +691 -0
- package/package.json +64 -4
- package/templates/init/Caddyfile +6 -0
- package/templates/init/DEPLOY.md +233 -0
- package/templates/init/Dockerfile +12 -0
- package/templates/init/app.json +21 -0
- package/templates/init/aws/README.md +118 -0
- package/templates/init/aws/ecs-fargate-alb.yml +207 -0
- package/templates/init/aws/ecs-fargate-nlb-quic.yml +210 -0
- package/templates/init/babylon-game-server.config.mjs +158 -0
- package/templates/init/colyseus-cloud/README.md +133 -0
- package/templates/init/colyseus-cloud/babylon-game-server.config.mjs +36 -0
- package/templates/init/colyseus-cloud/ecosystem.config.cjs +35 -0
- package/templates/init/colyseus-cloud/gitignore +6 -0
- package/templates/init/colyseus-cloud/package.json +15 -0
- package/templates/init/digitalocean-app.yaml +30 -0
- package/templates/init/docker-compose.full.yml +45 -0
- package/templates/init/docker-compose.websocket.yml +41 -0
- package/templates/init/fly.toml +56 -0
- package/templates/init/heroku.yml +11 -0
- package/templates/init/kubernetes/deployment.yaml +71 -0
- package/templates/init/kubernetes/redis.yaml +73 -0
- package/templates/init/kubernetes/service.yaml +24 -0
- package/templates/init/monitoring/grafana-dashboard.json +539 -0
- package/templates/init/monitoring/prometheus-alerts.yml +51 -0
- package/templates/init/monitoring/prometheus-scrape.yml +17 -0
- package/templates/init/railway.json +13 -0
- package/templates/init/render.yaml +19 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { type AdminCaller } from "./AdminApiRouter.js";
|
|
2
|
+
import type { ConfigChangeView, ConfigOverridesRequest, ConfigView } from "./AdminApiTypes.js";
|
|
3
|
+
import type { AdminCoreServices } from "./AdminServices.js";
|
|
4
|
+
import type { FleetDirectory } from "./FleetDirectory.js";
|
|
5
|
+
/** The settings document of the overrides. */
|
|
6
|
+
export declare const CONFIG_OVERRIDES_SETTINGS_ID: string;
|
|
7
|
+
/** The export formats of exportSnippet. */
|
|
8
|
+
export type ConfigExportFormat = "mjs" | "env";
|
|
9
|
+
/** The console's live overrides of this instance. */
|
|
10
|
+
export declare class ConfigOverrides {
|
|
11
|
+
/** The shared core services. */
|
|
12
|
+
private readonly services;
|
|
13
|
+
/** Announces a change, so every instance re-applies the overrides. */
|
|
14
|
+
private readonly fleet;
|
|
15
|
+
/** The config before any override (R32), which a revert restores. */
|
|
16
|
+
private baseConfig;
|
|
17
|
+
/** The overrides as last read from the store. */
|
|
18
|
+
private stored;
|
|
19
|
+
/** The revision of the stored document, or null when none is stored. */
|
|
20
|
+
private revision;
|
|
21
|
+
/** The paths an override currently replaces on this instance. */
|
|
22
|
+
private readonly overriddenPaths;
|
|
23
|
+
/**
|
|
24
|
+
* Prepares the overrides; nothing changes until applyStored().
|
|
25
|
+
* @param services The shared core services.
|
|
26
|
+
* @param fleet The fleet directory (change announcements).
|
|
27
|
+
*/
|
|
28
|
+
constructor(services: AdminCoreServices, fleet: FleetDirectory);
|
|
29
|
+
/**
|
|
30
|
+
* Keeps the config before any override, then applies the stored overrides (at boot, before the server listens;
|
|
31
|
+
* instance overrides apply once the process id is known, on the next reload).
|
|
32
|
+
*/
|
|
33
|
+
applyStored(): Promise<void>;
|
|
34
|
+
/** Reads the stored overrides again and applies them (a settings change announcement). */
|
|
35
|
+
reload(): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Overrides a live option, fleet-wide or on one instance (D20, DR §9).
|
|
38
|
+
* @param request The scope, option path, value and the revision the editor read.
|
|
39
|
+
* @param by Who changes it (recorded in the history).
|
|
40
|
+
* @returns The config page of the scope's instance (this one for the fleet).
|
|
41
|
+
* @throws AdminRequestError 400 for an option that needs a restart or an invalid value, 409 for an option set by an
|
|
42
|
+
* environment variable or flag; AdminConflictError for a stale expectedRevision.
|
|
43
|
+
*/
|
|
44
|
+
setOverride(request: ConfigOverridesRequest, by: AdminCaller): Promise<ConfigView>;
|
|
45
|
+
/**
|
|
46
|
+
* Removes an override, so the file, environment or default value applies again.
|
|
47
|
+
* @param scope "fleet" or a process id.
|
|
48
|
+
* @param path The option path.
|
|
49
|
+
* @param by Who reverts it (recorded in the history).
|
|
50
|
+
* @returns The config page of the scope's instance.
|
|
51
|
+
* @throws AdminRequestError 400 for an option that is not live-editable, 404 when the scope has no override of it.
|
|
52
|
+
*/
|
|
53
|
+
revert(scope: string, path: string, by: AdminCaller): Promise<ConfigView>;
|
|
54
|
+
/**
|
|
55
|
+
* The value an option has for a scope: the scope's override, else the fleet's, else the configured one.
|
|
56
|
+
* @param scope "fleet" or a process id.
|
|
57
|
+
* @param path The option path.
|
|
58
|
+
* @returns The value.
|
|
59
|
+
*/
|
|
60
|
+
valueAt(scope: string, path: string): unknown;
|
|
61
|
+
/**
|
|
62
|
+
* The config page (spec 9): every option with its value (secrets only "set" / "not set"), its source and whether
|
|
63
|
+
* it can change live, as it applies to an instance.
|
|
64
|
+
* @param processId The instance (this instance's base config and sources, with the overrides that apply there).
|
|
65
|
+
* @returns The page data.
|
|
66
|
+
*/
|
|
67
|
+
describe(processId: string): ConfigView;
|
|
68
|
+
/**
|
|
69
|
+
* The change history, newest first.
|
|
70
|
+
* @returns The last HISTORY_LENGTH changes.
|
|
71
|
+
*/
|
|
72
|
+
history(): Promise<ConfigChangeView[]>;
|
|
73
|
+
/**
|
|
74
|
+
* A config snippet with this instance's current settings, for restart-only options and for keeping live changes:
|
|
75
|
+
* every value that differs from the default, never a secret value.
|
|
76
|
+
* @param format "mjs" for babylon-game-server.config.mjs (set secrets read from process.env), "env" for .env lines
|
|
77
|
+
* (every secret listed with an empty value).
|
|
78
|
+
* @returns The file text.
|
|
79
|
+
*/
|
|
80
|
+
exportSnippet(format: ConfigExportFormat): string;
|
|
81
|
+
/**
|
|
82
|
+
* The option of a path, when the console may change it now.
|
|
83
|
+
* @param path The option path.
|
|
84
|
+
* @returns The option.
|
|
85
|
+
* @throws AdminRequestError 400 when it is not live-editable, 409 when an environment variable or flag set it.
|
|
86
|
+
*/
|
|
87
|
+
private requireEditable;
|
|
88
|
+
/**
|
|
89
|
+
* Checks that a scope is the fleet or a live instance.
|
|
90
|
+
* @param scope "fleet" or a process id.
|
|
91
|
+
* @throws AdminRequestError 404 for an unknown or lost instance.
|
|
92
|
+
*/
|
|
93
|
+
private requireKnownScope;
|
|
94
|
+
/**
|
|
95
|
+
* Parses a value by the option's type and checks it on a copy of the config.
|
|
96
|
+
* @param definition The option.
|
|
97
|
+
* @param value The value given.
|
|
98
|
+
* @returns The parsed value.
|
|
99
|
+
* @throws AdminRequestError 400 with the validation problems.
|
|
100
|
+
*/
|
|
101
|
+
private validatedValue;
|
|
102
|
+
/**
|
|
103
|
+
* Stores the overrides, applies them here and announces the change to every instance.
|
|
104
|
+
* @param next The new overrides.
|
|
105
|
+
* @param expectedRevision The revision the editor read, or null for no check.
|
|
106
|
+
* @throws AdminConflictError when expectedRevision is stale.
|
|
107
|
+
*/
|
|
108
|
+
private save;
|
|
109
|
+
/**
|
|
110
|
+
* Appends a change to the configHistory stream.
|
|
111
|
+
* @param change The change.
|
|
112
|
+
*/
|
|
113
|
+
private recordChange;
|
|
114
|
+
/**
|
|
115
|
+
* Applies the overrides that reach this instance and restores the base value of any that no longer do. Locked
|
|
116
|
+
* options are never touched, and a value that did not change is not written again (so a settings reload never
|
|
117
|
+
* resets a temporary log level).
|
|
118
|
+
*/
|
|
119
|
+
private applyEffective;
|
|
120
|
+
/**
|
|
121
|
+
* Writes a value into the running config and applies its side effect (D20): the logger's level, the join rate
|
|
122
|
+
* limiter; every other live option is read where it is used (maxMessagesPerSecond by new sessions).
|
|
123
|
+
* @param path The option path.
|
|
124
|
+
* @param value The value.
|
|
125
|
+
*/
|
|
126
|
+
private applyValue;
|
|
127
|
+
/**
|
|
128
|
+
* The override of a path that reaches an instance: its own, else the fleet's.
|
|
129
|
+
* @param path The option path.
|
|
130
|
+
* @param processId The instance, or null for the fleet value only.
|
|
131
|
+
* @returns The override, or null when none applies.
|
|
132
|
+
*/
|
|
133
|
+
private overrideFor;
|
|
134
|
+
/**
|
|
135
|
+
* Whether an environment variable or a flag set an option (it is then never overridden, D20).
|
|
136
|
+
* @param path The option path.
|
|
137
|
+
* @returns True when locked.
|
|
138
|
+
*/
|
|
139
|
+
private isLocked;
|
|
140
|
+
/**
|
|
141
|
+
* One option of the config page.
|
|
142
|
+
* @param definition The option.
|
|
143
|
+
* @param processId The instance the page describes.
|
|
144
|
+
* @returns The row.
|
|
145
|
+
*/
|
|
146
|
+
private describeOption;
|
|
147
|
+
/**
|
|
148
|
+
* The .env export: one line per non-default, non-secret value, then every secret with an empty value.
|
|
149
|
+
* @returns The text.
|
|
150
|
+
*/
|
|
151
|
+
private exportEnvironment;
|
|
152
|
+
/**
|
|
153
|
+
* The babylon-game-server.config.mjs export: every non-default, non-secret value, the dedicated scenes, and each set
|
|
154
|
+
* secret read from its environment variable.
|
|
155
|
+
* @returns The module text.
|
|
156
|
+
*/
|
|
157
|
+
private exportModule;
|
|
158
|
+
/**
|
|
159
|
+
* The non-secret options whose current value differs from the default.
|
|
160
|
+
* @returns The options.
|
|
161
|
+
*/
|
|
162
|
+
private changedOptions;
|
|
163
|
+
/**
|
|
164
|
+
* The instance a scope's page describes.
|
|
165
|
+
* @param scope "fleet" or a process id.
|
|
166
|
+
* @returns The process id (this instance for the fleet).
|
|
167
|
+
*/
|
|
168
|
+
private processIdOfScope;
|
|
169
|
+
/**
|
|
170
|
+
* The current time as ISO text.
|
|
171
|
+
* @returns The time.
|
|
172
|
+
*/
|
|
173
|
+
private nowText;
|
|
174
|
+
}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// Live config overrides from the console (game-server-admin-console D20, DR §9): only LIVE_EDITABLE_OPTION_PATHS, never
|
|
2
|
+
// an option set by an environment variable or a flag, validated on a copy of the config, stored fleet-wide or per
|
|
3
|
+
// instance in settings/configOverrides (so they survive restarts), re-applied by every instance on each change, and
|
|
4
|
+
// kept in the configHistory stream. Nothing is ever written to the config file on disk.
|
|
5
|
+
import { CONFIG_OPTIONS, DEFAULT_GAME_SERVER_CONFIG, parseOptionValue, readPath, validateGameServerConfig, writePath } from "../GameServerConfig.js";
|
|
6
|
+
import { getLogger } from "../GameServerLogger.js";
|
|
7
|
+
import { AdminRequestError } from "./AdminApiRouter.js";
|
|
8
|
+
import { LIVE_EDITABLE_OPTION_PATHS } from "./AdminConstants.js";
|
|
9
|
+
import { dedicatedSceneView } from "./DedicatedRoutes.js";
|
|
10
|
+
/** The settings document of the overrides. */
|
|
11
|
+
export const CONFIG_OVERRIDES_SETTINGS_ID = "configOverrides";
|
|
12
|
+
/** HTTP status of an invalid override. */
|
|
13
|
+
const HTTP_BAD_REQUEST = 400;
|
|
14
|
+
/** HTTP status of a revert with nothing to revert. */
|
|
15
|
+
const HTTP_NOT_FOUND = 404;
|
|
16
|
+
/** HTTP status of an override of a locked option. */
|
|
17
|
+
const HTTP_CONFLICT = 409;
|
|
18
|
+
/** The scope value of the whole fleet. */
|
|
19
|
+
const FLEET_SCOPE = "fleet";
|
|
20
|
+
/** How many config changes the history answers with. */
|
|
21
|
+
const HISTORY_LENGTH = 500;
|
|
22
|
+
/** What a secret that has a value shows. */
|
|
23
|
+
const SECRET_SET = "set";
|
|
24
|
+
/** What a secret without a value shows. */
|
|
25
|
+
const SECRET_NOT_SET = "not set";
|
|
26
|
+
/** Marks a list secret's environment-variable placeholder inside the JSON of a config module export. */
|
|
27
|
+
const LIST_PLACEHOLDER_PATTERN = /"__ENVIRONMENT_LIST_PLACEHOLDER__([A-Z0-9_]+)__"/g;
|
|
28
|
+
/** Marks a text secret's environment-variable placeholder inside the JSON of a config module export. */
|
|
29
|
+
const TEXT_PLACEHOLDER_PATTERN = /"__ENVIRONMENT_PLACEHOLDER__([A-Z0-9_]+)__"/g;
|
|
30
|
+
/** Live options whose change also needs a call besides the config write. */
|
|
31
|
+
const LOG_LEVEL_PATH = "logLevel";
|
|
32
|
+
const JOIN_RATE_PATH = "limits.joinRequestsPerMinutePerAddress";
|
|
33
|
+
/** The console's live overrides of this instance. */
|
|
34
|
+
export class ConfigOverrides {
|
|
35
|
+
/** The shared core services. */
|
|
36
|
+
services;
|
|
37
|
+
/** Announces a change, so every instance re-applies the overrides. */
|
|
38
|
+
fleet;
|
|
39
|
+
/** The config before any override (R32), which a revert restores. */
|
|
40
|
+
baseConfig = null;
|
|
41
|
+
/** The overrides as last read from the store. */
|
|
42
|
+
stored = emptyOverrides();
|
|
43
|
+
/** The revision of the stored document, or null when none is stored. */
|
|
44
|
+
revision = null;
|
|
45
|
+
/** The paths an override currently replaces on this instance. */
|
|
46
|
+
overriddenPaths = new Set();
|
|
47
|
+
/**
|
|
48
|
+
* Prepares the overrides; nothing changes until applyStored().
|
|
49
|
+
* @param services The shared core services.
|
|
50
|
+
* @param fleet The fleet directory (change announcements).
|
|
51
|
+
*/
|
|
52
|
+
constructor(services, fleet) {
|
|
53
|
+
this.services = services;
|
|
54
|
+
this.fleet = fleet;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Keeps the config before any override, then applies the stored overrides (at boot, before the server listens;
|
|
58
|
+
* instance overrides apply once the process id is known, on the next reload).
|
|
59
|
+
*/
|
|
60
|
+
async applyStored() {
|
|
61
|
+
this.baseConfig ??= structuredClone(this.services.state.config);
|
|
62
|
+
await this.reload();
|
|
63
|
+
}
|
|
64
|
+
/** Reads the stored overrides again and applies them (a settings change announcement). */
|
|
65
|
+
async reload() {
|
|
66
|
+
const stored = await this.services.store.getDocument("settings", CONFIG_OVERRIDES_SETTINGS_ID);
|
|
67
|
+
this.stored = stored?.value ?? emptyOverrides();
|
|
68
|
+
this.revision = stored?.revision ?? null;
|
|
69
|
+
this.applyEffective();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Overrides a live option, fleet-wide or on one instance (D20, DR §9).
|
|
73
|
+
* @param request The scope, option path, value and the revision the editor read.
|
|
74
|
+
* @param by Who changes it (recorded in the history).
|
|
75
|
+
* @returns The config page of the scope's instance (this one for the fleet).
|
|
76
|
+
* @throws AdminRequestError 400 for an option that needs a restart or an invalid value, 409 for an option set by an
|
|
77
|
+
* environment variable or flag; AdminConflictError for a stale expectedRevision.
|
|
78
|
+
*/
|
|
79
|
+
async setOverride(request, by) {
|
|
80
|
+
const definition = this.requireEditable(request.path);
|
|
81
|
+
const value = this.validatedValue(definition, request.value);
|
|
82
|
+
await this.requireKnownScope(request.scope);
|
|
83
|
+
await this.reload();
|
|
84
|
+
const before = this.valueAt(request.scope, request.path);
|
|
85
|
+
const next = structuredClone(this.stored);
|
|
86
|
+
overridesOfScope(next, request.scope)[request.path] = value;
|
|
87
|
+
await this.save(next, request.expectedRevision);
|
|
88
|
+
await this.recordChange({ at: this.nowText(), by: by.name, scope: request.scope, path: request.path, before: before, after: value, reverted: false });
|
|
89
|
+
return this.describe(this.processIdOfScope(request.scope));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Removes an override, so the file, environment or default value applies again.
|
|
93
|
+
* @param scope "fleet" or a process id.
|
|
94
|
+
* @param path The option path.
|
|
95
|
+
* @param by Who reverts it (recorded in the history).
|
|
96
|
+
* @returns The config page of the scope's instance.
|
|
97
|
+
* @throws AdminRequestError 400 for an option that is not live-editable, 404 when the scope has no override of it.
|
|
98
|
+
*/
|
|
99
|
+
async revert(scope, path, by) {
|
|
100
|
+
if (!LIVE_EDITABLE_OPTION_PATHS.includes(path))
|
|
101
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "this option needs a restart");
|
|
102
|
+
await this.reload();
|
|
103
|
+
const next = structuredClone(this.stored);
|
|
104
|
+
const scoped = overridesOfScope(next, scope);
|
|
105
|
+
if (!Object.hasOwn(scoped, path))
|
|
106
|
+
throw new AdminRequestError(HTTP_NOT_FOUND, "this scope has no override of " + path);
|
|
107
|
+
const before = this.valueAt(scope, path);
|
|
108
|
+
delete scoped[path];
|
|
109
|
+
if (scope !== FLEET_SCOPE && Object.keys(scoped).length === 0)
|
|
110
|
+
delete next.instances[scope];
|
|
111
|
+
await this.save(next, this.revision);
|
|
112
|
+
await this.recordChange({ at: this.nowText(), by: by.name, scope: scope, path: path, before: before, after: this.valueAt(scope, path), reverted: true });
|
|
113
|
+
return this.describe(this.processIdOfScope(scope));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The value an option has for a scope: the scope's override, else the fleet's, else the configured one.
|
|
117
|
+
* @param scope "fleet" or a process id.
|
|
118
|
+
* @param path The option path.
|
|
119
|
+
* @returns The value.
|
|
120
|
+
*/
|
|
121
|
+
valueAt(scope, path) {
|
|
122
|
+
const override = this.overrideFor(path, (scope === FLEET_SCOPE) ? null : scope);
|
|
123
|
+
if (override != null)
|
|
124
|
+
return override.value;
|
|
125
|
+
return readPath(this.baseConfig ?? this.services.state.config, path);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The config page (spec 9): every option with its value (secrets only "set" / "not set"), its source and whether
|
|
129
|
+
* it can change live, as it applies to an instance.
|
|
130
|
+
* @param processId The instance (this instance's base config and sources, with the overrides that apply there).
|
|
131
|
+
* @returns The page data.
|
|
132
|
+
*/
|
|
133
|
+
describe(processId) {
|
|
134
|
+
return {
|
|
135
|
+
processId: processId,
|
|
136
|
+
revision: this.revision,
|
|
137
|
+
options: CONFIG_OPTIONS.map((definition) => this.describeOption(definition, processId)),
|
|
138
|
+
dedicatedScenes: this.services.state.registry.listScenes().map(dedicatedSceneView)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The change history, newest first.
|
|
143
|
+
* @returns The last HISTORY_LENGTH changes.
|
|
144
|
+
*/
|
|
145
|
+
async history() {
|
|
146
|
+
const records = await this.services.store.readRecords("configHistory", { limit: HISTORY_LENGTH, newestFirst: true });
|
|
147
|
+
return records.map((record) => record.value);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* A config snippet with this instance's current settings, for restart-only options and for keeping live changes:
|
|
151
|
+
* every value that differs from the default, never a secret value.
|
|
152
|
+
* @param format "mjs" for babylon-game-server.config.mjs (set secrets read from process.env), "env" for .env lines
|
|
153
|
+
* (every secret listed with an empty value).
|
|
154
|
+
* @returns The file text.
|
|
155
|
+
*/
|
|
156
|
+
exportSnippet(format) {
|
|
157
|
+
return (format === "env") ? this.exportEnvironment() : this.exportModule();
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The option of a path, when the console may change it now.
|
|
161
|
+
* @param path The option path.
|
|
162
|
+
* @returns The option.
|
|
163
|
+
* @throws AdminRequestError 400 when it is not live-editable, 409 when an environment variable or flag set it.
|
|
164
|
+
*/
|
|
165
|
+
requireEditable(path) {
|
|
166
|
+
const definition = CONFIG_OPTIONS.find((candidate) => candidate.path === path);
|
|
167
|
+
if (definition == null || !LIVE_EDITABLE_OPTION_PATHS.includes(path))
|
|
168
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "this option needs a restart");
|
|
169
|
+
const source = this.services.state.configSources[path];
|
|
170
|
+
if (source === "environment")
|
|
171
|
+
throw new AdminRequestError(HTTP_CONFLICT, "set by " + definition.environmentVariable + "; change it there", path);
|
|
172
|
+
if (source === "flag")
|
|
173
|
+
throw new AdminRequestError(HTTP_CONFLICT, "set by --" + (definition.flag ?? path) + "; change it there", path);
|
|
174
|
+
return definition;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Checks that a scope is the fleet or a live instance.
|
|
178
|
+
* @param scope "fleet" or a process id.
|
|
179
|
+
* @throws AdminRequestError 404 for an unknown or lost instance.
|
|
180
|
+
*/
|
|
181
|
+
async requireKnownScope(scope) {
|
|
182
|
+
if (scope === FLEET_SCOPE || scope === this.services.processId())
|
|
183
|
+
return;
|
|
184
|
+
const isLiveInstance = (await this.fleet.listInstances()).some((instance) => instance.processId === scope && instance.status === "up");
|
|
185
|
+
if (!isLiveInstance)
|
|
186
|
+
throw new AdminRequestError(HTTP_NOT_FOUND, "no such instance");
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Parses a value by the option's type and checks it on a copy of the config.
|
|
190
|
+
* @param definition The option.
|
|
191
|
+
* @param value The value given.
|
|
192
|
+
* @returns The parsed value.
|
|
193
|
+
* @throws AdminRequestError 400 with the validation problems.
|
|
194
|
+
*/
|
|
195
|
+
validatedValue(definition, value) {
|
|
196
|
+
const parsed = parseOptionValue(definition, value);
|
|
197
|
+
if (parsed == null)
|
|
198
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, definition.path + " cannot be " + JSON.stringify(value));
|
|
199
|
+
const candidate = structuredClone(this.services.state.config);
|
|
200
|
+
writePath(candidate, definition.path, parsed.value);
|
|
201
|
+
const problems = validateGameServerConfig(candidate);
|
|
202
|
+
if (problems.length > 0)
|
|
203
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, problems.join("; "));
|
|
204
|
+
return parsed.value;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Stores the overrides, applies them here and announces the change to every instance.
|
|
208
|
+
* @param next The new overrides.
|
|
209
|
+
* @param expectedRevision The revision the editor read, or null for no check.
|
|
210
|
+
* @throws AdminConflictError when expectedRevision is stale.
|
|
211
|
+
*/
|
|
212
|
+
async save(next, expectedRevision) {
|
|
213
|
+
const saved = await this.services.store.putDocument("settings", CONFIG_OVERRIDES_SETTINGS_ID, next, expectedRevision);
|
|
214
|
+
this.stored = saved.value;
|
|
215
|
+
this.revision = saved.revision;
|
|
216
|
+
this.applyEffective();
|
|
217
|
+
await this.fleet.announceChange("settings");
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Appends a change to the configHistory stream.
|
|
221
|
+
* @param change The change.
|
|
222
|
+
*/
|
|
223
|
+
async recordChange(change) {
|
|
224
|
+
await this.services.store.appendRecord("configHistory", change);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Applies the overrides that reach this instance and restores the base value of any that no longer do. Locked
|
|
228
|
+
* options are never touched, and a value that did not change is not written again (so a settings reload never
|
|
229
|
+
* resets a temporary log level).
|
|
230
|
+
*/
|
|
231
|
+
applyEffective() {
|
|
232
|
+
const base = this.baseConfig ?? this.services.state.config;
|
|
233
|
+
for (const path of LIVE_EDITABLE_OPTION_PATHS) {
|
|
234
|
+
const override = this.isLocked(path) ? null : this.overrideFor(path, this.services.processId() ?? null);
|
|
235
|
+
if (override != null) {
|
|
236
|
+
this.applyValue(path, override.value);
|
|
237
|
+
this.overriddenPaths.add(path);
|
|
238
|
+
}
|
|
239
|
+
else if (this.overriddenPaths.delete(path)) {
|
|
240
|
+
this.applyValue(path, structuredClone(readPath(base, path)));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Writes a value into the running config and applies its side effect (D20): the logger's level, the join rate
|
|
246
|
+
* limiter; every other live option is read where it is used (maxMessagesPerSecond by new sessions).
|
|
247
|
+
* @param path The option path.
|
|
248
|
+
* @param value The value.
|
|
249
|
+
*/
|
|
250
|
+
applyValue(path, value) {
|
|
251
|
+
const state = this.services.state;
|
|
252
|
+
if (JSON.stringify(readPath(state.config, path)) === JSON.stringify(value))
|
|
253
|
+
return;
|
|
254
|
+
writePath(state.config, path, value);
|
|
255
|
+
if (path === LOG_LEVEL_PATH)
|
|
256
|
+
getLogger().setLevel(state.config.logLevel);
|
|
257
|
+
if (path === JOIN_RATE_PATH)
|
|
258
|
+
state.rateLimiter.setRequestsPerMinute(state.config.limits.joinRequestsPerMinutePerAddress);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The override of a path that reaches an instance: its own, else the fleet's.
|
|
262
|
+
* @param path The option path.
|
|
263
|
+
* @param processId The instance, or null for the fleet value only.
|
|
264
|
+
* @returns The override, or null when none applies.
|
|
265
|
+
*/
|
|
266
|
+
overrideFor(path, processId) {
|
|
267
|
+
const instanceOverrides = (processId != null) ? this.stored.instances[processId] : undefined;
|
|
268
|
+
if (instanceOverrides != null && Object.hasOwn(instanceOverrides, path))
|
|
269
|
+
return { value: instanceOverrides[path] };
|
|
270
|
+
if (Object.hasOwn(this.stored.fleet, path))
|
|
271
|
+
return { value: this.stored.fleet[path] };
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Whether an environment variable or a flag set an option (it is then never overridden, D20).
|
|
276
|
+
* @param path The option path.
|
|
277
|
+
* @returns True when locked.
|
|
278
|
+
*/
|
|
279
|
+
isLocked(path) {
|
|
280
|
+
const source = this.services.state.configSources[path];
|
|
281
|
+
return source === "environment" || source === "flag";
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* One option of the config page.
|
|
285
|
+
* @param definition The option.
|
|
286
|
+
* @param processId The instance the page describes.
|
|
287
|
+
* @returns The row.
|
|
288
|
+
*/
|
|
289
|
+
describeOption(definition, processId) {
|
|
290
|
+
const liveEditable = LIVE_EDITABLE_OPTION_PATHS.includes(definition.path);
|
|
291
|
+
const locked = this.isLocked(definition.path);
|
|
292
|
+
const override = (liveEditable && !locked) ? this.overrideFor(definition.path, processId) : null;
|
|
293
|
+
const value = (override != null) ? override.value : readPath(this.baseConfig ?? this.services.state.config, definition.path);
|
|
294
|
+
return {
|
|
295
|
+
path: definition.path,
|
|
296
|
+
environmentVariable: definition.environmentVariable,
|
|
297
|
+
flag: definition.flag,
|
|
298
|
+
description: definition.description,
|
|
299
|
+
isSecret: definition.isSecret,
|
|
300
|
+
value: definition.isSecret ? (hasValue(value) ? SECRET_SET : SECRET_NOT_SET) : value,
|
|
301
|
+
source: (override != null) ? "override" : (this.services.state.configSources[definition.path] ?? "code"),
|
|
302
|
+
liveEditable: liveEditable,
|
|
303
|
+
locked: locked,
|
|
304
|
+
needsRestart: !liveEditable
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* The .env export: one line per non-default, non-secret value, then every secret with an empty value.
|
|
309
|
+
* @returns The text.
|
|
310
|
+
*/
|
|
311
|
+
exportEnvironment() {
|
|
312
|
+
const lines = ["# Babylon Game Server settings exported from the admin console on " + this.nowText()];
|
|
313
|
+
for (const definition of this.changedOptions())
|
|
314
|
+
lines.push(definition.environmentVariable + "=" + environmentText(readPath(this.services.state.config, definition.path)));
|
|
315
|
+
lines.push("# Secrets are never exported: fill these in where they are needed.");
|
|
316
|
+
for (const definition of CONFIG_OPTIONS.filter((candidate) => candidate.isSecret))
|
|
317
|
+
lines.push(definition.environmentVariable + "=");
|
|
318
|
+
return lines.join("\n") + "\n";
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* The babylon-game-server.config.mjs export: every non-default, non-secret value, the dedicated scenes, and each set
|
|
322
|
+
* secret read from its environment variable.
|
|
323
|
+
* @returns The module text.
|
|
324
|
+
*/
|
|
325
|
+
exportModule() {
|
|
326
|
+
const config = this.services.state.config;
|
|
327
|
+
const exported = {};
|
|
328
|
+
for (const definition of this.changedOptions())
|
|
329
|
+
writePath(exported, definition.path, readPath(config, definition.path));
|
|
330
|
+
for (const definition of CONFIG_OPTIONS.filter((candidate) => candidate.isSecret && hasValue(readPath(config, candidate.path)))) {
|
|
331
|
+
const placeholderKind = (definition.valueType === "string-list") ? "__ENVIRONMENT_LIST_PLACEHOLDER__" : "__ENVIRONMENT_PLACEHOLDER__";
|
|
332
|
+
writePath(exported, definition.path, placeholderKind + definition.environmentVariable + "__");
|
|
333
|
+
}
|
|
334
|
+
if (config.dedicated.scenes.length > 0 || config.dedicated.boot.length > 0)
|
|
335
|
+
exported.dedicated = structuredClone(config.dedicated);
|
|
336
|
+
const body = JSON.stringify(exported, null, 4)
|
|
337
|
+
.replace(LIST_PLACEHOLDER_PATTERN, "(process.env.$1 ?? \"\").split(\",\")")
|
|
338
|
+
.replace(TEXT_PLACEHOLDER_PATTERN, "process.env.$1 ?? null");
|
|
339
|
+
return "// Babylon Game Server settings exported from the admin console on " + this.nowText() + ".\n// Secrets are read from the environment, never written here.\nexport default " + body + ";\n";
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* The non-secret options whose current value differs from the default.
|
|
343
|
+
* @returns The options.
|
|
344
|
+
*/
|
|
345
|
+
changedOptions() {
|
|
346
|
+
return CONFIG_OPTIONS.filter((definition) => {
|
|
347
|
+
if (definition.isSecret)
|
|
348
|
+
return false;
|
|
349
|
+
return JSON.stringify(readPath(this.services.state.config, definition.path)) !== JSON.stringify(readPath(DEFAULT_GAME_SERVER_CONFIG, definition.path));
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* The instance a scope's page describes.
|
|
354
|
+
* @param scope "fleet" or a process id.
|
|
355
|
+
* @returns The process id (this instance for the fleet).
|
|
356
|
+
*/
|
|
357
|
+
processIdOfScope(scope) {
|
|
358
|
+
return (scope === FLEET_SCOPE) ? this.services.processId() : scope;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* The current time as ISO text.
|
|
362
|
+
* @returns The time.
|
|
363
|
+
*/
|
|
364
|
+
nowText() {
|
|
365
|
+
return new Date(this.services.clock.now()).toISOString();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* No overrides.
|
|
370
|
+
* @returns An empty overrides document.
|
|
371
|
+
*/
|
|
372
|
+
function emptyOverrides() {
|
|
373
|
+
return { fleet: {}, instances: {} };
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* The override map of a scope, created when missing.
|
|
377
|
+
* @param overrides The overrides document (changed in place).
|
|
378
|
+
* @param scope "fleet" or a process id.
|
|
379
|
+
* @returns The scope's map.
|
|
380
|
+
*/
|
|
381
|
+
function overridesOfScope(overrides, scope) {
|
|
382
|
+
if (scope === FLEET_SCOPE)
|
|
383
|
+
return overrides.fleet;
|
|
384
|
+
overrides.instances[scope] ??= {};
|
|
385
|
+
return overrides.instances[scope];
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Whether an option has a value (a secret is "set").
|
|
389
|
+
* @param value The value.
|
|
390
|
+
* @returns False for null, undefined, "" and an empty list.
|
|
391
|
+
*/
|
|
392
|
+
function hasValue(value) {
|
|
393
|
+
if (Array.isArray(value))
|
|
394
|
+
return value.length > 0;
|
|
395
|
+
return value != null && value !== "";
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* A value as .env text: lists comma-separated, null empty.
|
|
399
|
+
* @param value The value.
|
|
400
|
+
* @returns The text.
|
|
401
|
+
*/
|
|
402
|
+
function environmentText(value) {
|
|
403
|
+
if (Array.isArray(value))
|
|
404
|
+
return value.join(",");
|
|
405
|
+
return (value == null) ? "" : String(value);
|
|
406
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type AdminApiRouter } from "./AdminApiRouter.js";
|
|
2
|
+
import type { AdminServices } from "./AdminServices.js";
|
|
3
|
+
/**
|
|
4
|
+
* Registers the config/* routes.
|
|
5
|
+
* @param router The admin API router.
|
|
6
|
+
* @param services The shared services.
|
|
7
|
+
*/
|
|
8
|
+
export declare function registerConfigRoutes(router: AdminApiRouter, services: AdminServices): void;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// The config routes (game-server-admin-console DR §6.11 config rows, D20): the config page with every option's source,
|
|
2
|
+
// live overrides and their revert, the change history and the export of a config file or .env snippet.
|
|
3
|
+
import { AdminRequestError } from "./AdminApiRouter.js";
|
|
4
|
+
import { bodyObject, stringField } from "./AdminRequestBodies.js";
|
|
5
|
+
/** HTTP status of a successful read or change. */
|
|
6
|
+
const HTTP_OK = 200;
|
|
7
|
+
/** HTTP status of an invalid request. */
|
|
8
|
+
const HTTP_BAD_REQUEST = 400;
|
|
9
|
+
/** The download of each export format. */
|
|
10
|
+
const EXPORT_FILES = {
|
|
11
|
+
mjs: { fileName: "babylon-game-server.config.mjs", contentType: "text/javascript; charset=utf-8" },
|
|
12
|
+
env: { fileName: ".env", contentType: "text/plain; charset=utf-8" }
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Registers the config/* routes.
|
|
16
|
+
* @param router The admin API router.
|
|
17
|
+
* @param services The shared services.
|
|
18
|
+
*/
|
|
19
|
+
export function registerConfigRoutes(router, services) {
|
|
20
|
+
const routes = [
|
|
21
|
+
{ method: "GET", pattern: "config", permission: "config.view", requiresStepUp: false, auditAction: null, handler: async (request) => ({ status: HTTP_OK, body: services.configOverrides.describe(request.query.get("processId") || services.fleet.localProcessId) }) },
|
|
22
|
+
{ method: "PUT", pattern: "config/overrides", permission: "config.manage", requiresStepUp: false, auditAction: "config.override", handler: async (request) => setOverride(services, request) },
|
|
23
|
+
{ method: "DELETE", pattern: "config/overrides", permission: "config.manage", requiresStepUp: false, auditAction: "config.revert", handler: async (request) => revertOverride(services, request) },
|
|
24
|
+
{ method: "GET", pattern: "config/history", permission: "config.view", requiresStepUp: false, auditAction: null, handler: async () => ({ status: HTTP_OK, body: await services.configOverrides.history() }) },
|
|
25
|
+
{ method: "GET", pattern: "config/export", permission: "config.view", requiresStepUp: false, auditAction: null, handler: async (request) => exportConfig(services, request) }
|
|
26
|
+
];
|
|
27
|
+
for (const route of routes)
|
|
28
|
+
router.register(route);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* PUT config/overrides: overrides a live option, audited with its value before and after.
|
|
32
|
+
* @param services The shared services.
|
|
33
|
+
* @param request The request.
|
|
34
|
+
* @returns The config page.
|
|
35
|
+
* @throws AdminRequestError 400 for a bad body (and the ConfigOverrides refusals).
|
|
36
|
+
*/
|
|
37
|
+
async function setOverride(services, request) {
|
|
38
|
+
const body = bodyObject(request.body);
|
|
39
|
+
const expectedRevision = body.expectedRevision ?? null;
|
|
40
|
+
if (expectedRevision !== null && !Number.isInteger(expectedRevision))
|
|
41
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "expectedRevision must be a whole number or null");
|
|
42
|
+
if (!Object.hasOwn(body, "value"))
|
|
43
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "value is missing");
|
|
44
|
+
const change = { scope: stringField(body, "scope"), path: stringField(body, "path"), value: body.value, expectedRevision: expectedRevision };
|
|
45
|
+
const before = services.configOverrides.valueAt(change.scope, change.path);
|
|
46
|
+
const view = await services.configOverrides.setOverride(change, requireCaller(request));
|
|
47
|
+
const after = services.configOverrides.valueAt(change.scope, change.path);
|
|
48
|
+
return { status: HTTP_OK, body: view, audit: { target: change.path, parameters: { scope: change.scope, before: before, after: after } } };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* DELETE config/overrides?scope=&path=: removes an override, audited with its value before and after.
|
|
52
|
+
* @param services The shared services.
|
|
53
|
+
* @param request The request.
|
|
54
|
+
* @returns The config page.
|
|
55
|
+
* @throws AdminRequestError 400 without scope or path.
|
|
56
|
+
*/
|
|
57
|
+
async function revertOverride(services, request) {
|
|
58
|
+
const scope = request.query.get("scope");
|
|
59
|
+
const path = request.query.get("path");
|
|
60
|
+
if (scope == null || scope === "" || path == null || path === "")
|
|
61
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "scope and path are required");
|
|
62
|
+
const before = services.configOverrides.valueAt(scope, path);
|
|
63
|
+
const view = await services.configOverrides.revert(scope, path, requireCaller(request));
|
|
64
|
+
return { status: HTTP_OK, body: view, audit: { target: path, parameters: { scope: scope, before: before, after: services.configOverrides.valueAt(scope, path) } } };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* GET config/export?format=mjs|env: this instance's settings as a config file or .env lines, without secret values.
|
|
68
|
+
* @param services The shared services.
|
|
69
|
+
* @param request The request.
|
|
70
|
+
* @returns The download.
|
|
71
|
+
* @throws AdminRequestError 400 for an unknown format.
|
|
72
|
+
*/
|
|
73
|
+
async function exportConfig(services, request) {
|
|
74
|
+
const format = request.query.get("format") ?? "mjs";
|
|
75
|
+
if (!Object.hasOwn(EXPORT_FILES, format))
|
|
76
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "format must be mjs or env");
|
|
77
|
+
const exportFormat = format;
|
|
78
|
+
const bytes = new TextEncoder().encode(services.configOverrides.exportSnippet(exportFormat));
|
|
79
|
+
return { status: HTTP_OK, body: null, download: { bytes: bytes, contentType: EXPORT_FILES[exportFormat].contentType, fileName: EXPORT_FILES[exportFormat].fileName } };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The caller of an authorized request (the pipeline always sets it for permission routes).
|
|
83
|
+
* @param request The request.
|
|
84
|
+
* @returns The caller.
|
|
85
|
+
* @throws Error when the pipeline left the caller unset (a programming error).
|
|
86
|
+
*/
|
|
87
|
+
function requireCaller(request) {
|
|
88
|
+
if (request.caller == null)
|
|
89
|
+
throw new Error("an authorized admin route ran without a caller");
|
|
90
|
+
return request.caller;
|
|
91
|
+
}
|