@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.
- 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,92 @@
|
|
|
1
|
+
// The recent log lines of one instance (game-server-admin-console D21, DR §6.8): a fixed-size in-memory ring the
|
|
2
|
+
// logger feeds through a sink, redacted at capture, read by the console's live tail and search. Nothing goes to disk.
|
|
3
|
+
import { redactLogFields, redactLogMessage } from "./LogRedaction.js";
|
|
4
|
+
/** Fields that hold a client's address, removed for callers without players.viewAddresses. */
|
|
5
|
+
const ADDRESS_FIELDS = ["clientAddress", "address"];
|
|
6
|
+
/** Severity of each level: an entry matches a level filter when its rank is at most the filter's (error 0 … debug 3). */
|
|
7
|
+
const LEVEL_RANK = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
8
|
+
/** The in-memory log buffer of this instance. */
|
|
9
|
+
export class LogBuffer {
|
|
10
|
+
/** The logger sink that captures into this buffer (add it with GameServerLogger.addSink). */
|
|
11
|
+
sink = (level, message, fields) => this.capture(level, message, fields);
|
|
12
|
+
/** The entries, oldest first. */
|
|
13
|
+
entries = [];
|
|
14
|
+
/** How many entries are kept. */
|
|
15
|
+
capacity;
|
|
16
|
+
/** The sequence of the newest entry (0 before any). */
|
|
17
|
+
newestSequence = 0;
|
|
18
|
+
/**
|
|
19
|
+
* Creates an empty buffer.
|
|
20
|
+
* @param capacity How many entries are kept (admin.logBufferEntries); the oldest are dropped first.
|
|
21
|
+
*/
|
|
22
|
+
constructor(capacity) {
|
|
23
|
+
this.capacity = Math.max(1, capacity);
|
|
24
|
+
}
|
|
25
|
+
/** The sequence of the newest entry, 0 before any. */
|
|
26
|
+
get latestSequence() {
|
|
27
|
+
return this.newestSequence;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Adds an entry, redacted (DR §6.8), with the next sequence and the current time.
|
|
31
|
+
* @param level The entry's level.
|
|
32
|
+
* @param message What happened.
|
|
33
|
+
* @param fields The entry's fields.
|
|
34
|
+
*/
|
|
35
|
+
capture(level, message, fields) {
|
|
36
|
+
this.newestSequence += 1;
|
|
37
|
+
this.entries.push({ sequence: this.newestSequence, time: new Date().toISOString(), level: level, message: redactLogMessage(message), fields: redactLogFields(fields) });
|
|
38
|
+
if (this.entries.length > this.capacity)
|
|
39
|
+
this.entries.shift();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Reads matching entries. With a cursor, the oldest entries after it come first, so a live tail misses nothing;
|
|
43
|
+
* without one (0), the newest entries are returned, so a first look shows what just happened.
|
|
44
|
+
* @param filter What to keep.
|
|
45
|
+
* @param afterSequence Only entries with a larger sequence; 0 for the newest.
|
|
46
|
+
* @param limit The most entries returned.
|
|
47
|
+
* @returns The entries, oldest first.
|
|
48
|
+
*/
|
|
49
|
+
read(filter, afterSequence, limit) {
|
|
50
|
+
const matches = entryFilter(filter);
|
|
51
|
+
const found = this.entries.filter((entry) => entry.sequence > afterSequence && matches(entry));
|
|
52
|
+
const kept = (afterSequence > 0) ? found.slice(0, limit) : found.slice(Math.max(0, found.length - limit));
|
|
53
|
+
return kept.map((entry) => structuredClone(entry));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A buffered entry as the console shows it.
|
|
58
|
+
* @param entry The entry.
|
|
59
|
+
* @param processId The instance it came from.
|
|
60
|
+
* @param includeAddresses False removes the client address fields (callers without players.viewAddresses).
|
|
61
|
+
* @returns The view.
|
|
62
|
+
*/
|
|
63
|
+
export function toLogEntryView(entry, processId, includeAddresses) {
|
|
64
|
+
const fields = Object.assign({}, entry.fields);
|
|
65
|
+
if (!includeAddresses)
|
|
66
|
+
for (const field of ADDRESS_FIELDS)
|
|
67
|
+
delete fields[field];
|
|
68
|
+
return { sequence: entry.sequence, time: entry.time, level: entry.level, message: entry.message, fields: fields, processId: processId };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The test of a log filter.
|
|
72
|
+
* @param filter The filter.
|
|
73
|
+
* @returns True for the entries it keeps.
|
|
74
|
+
*/
|
|
75
|
+
function entryFilter(filter) {
|
|
76
|
+
const text = (filter.text != null && filter.text !== "") ? filter.text.toLowerCase() : null;
|
|
77
|
+
return (entry) => {
|
|
78
|
+
if (filter.level != null && LEVEL_RANK[entry.level] > LEVEL_RANK[filter.level])
|
|
79
|
+
return false;
|
|
80
|
+
if (filter.roomId != null && entry.fields.roomId !== filter.roomId)
|
|
81
|
+
return false;
|
|
82
|
+
if (filter.sessionName != null && entry.fields.sessionName !== filter.sessionName)
|
|
83
|
+
return false;
|
|
84
|
+
if (filter.playerId != null && entry.fields.playerId !== filter.playerId)
|
|
85
|
+
return false;
|
|
86
|
+
if (filter.userId != null && entry.fields.userId !== filter.userId)
|
|
87
|
+
return false;
|
|
88
|
+
if (text == null)
|
|
89
|
+
return true;
|
|
90
|
+
return entry.message.toLowerCase().includes(text) || JSON.stringify(entry.fields).toLowerCase().includes(text);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The text that replaces a secret. */
|
|
2
|
+
export declare const REDACTED_TEXT: string;
|
|
3
|
+
/**
|
|
4
|
+
* Removes secrets from a log message.
|
|
5
|
+
* @param message The message.
|
|
6
|
+
* @returns The message with every secret pattern replaced by "[redacted]".
|
|
7
|
+
*/
|
|
8
|
+
export declare function redactLogMessage(message: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Removes secrets from log fields: a field whose name looks secret becomes "[redacted]" (except the DR §6.8 names),
|
|
11
|
+
* and text anywhere inside the values is redacted like a message.
|
|
12
|
+
* @param fields The fields.
|
|
13
|
+
* @returns A redacted copy.
|
|
14
|
+
*/
|
|
15
|
+
export declare function redactLogFields(fields: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Redaction of captured log entries (game-server-admin-console D21, DR §6.8): secrets never reach the console's log
|
|
2
|
+
// buffer, whichever code logged them.
|
|
3
|
+
/** The text that replaces a secret. */
|
|
4
|
+
export const REDACTED_TEXT = "[redacted]";
|
|
5
|
+
/** Field names that hold secrets. */
|
|
6
|
+
const SECRET_FIELD_PATTERN = /(token|secret|password|passcode|code|key|authorization|cookie|pem|credential)/i;
|
|
7
|
+
/** Fields whose names look secret but are not (DR §6.8). */
|
|
8
|
+
const SAFE_FIELD_NAMES = new Set(["roomId", "sessionName", "playerId", "userId", "clientAddress", "reasonName", "statusCode"]);
|
|
9
|
+
/** Secret patterns inside text: API keys, bearer credentials and PEM blocks (DR §6.8). */
|
|
10
|
+
const SECRET_TEXT_PATTERNS = [
|
|
11
|
+
/bgsk_[A-Za-z0-9_-]+/g,
|
|
12
|
+
/Bearer\s+\S+/gi,
|
|
13
|
+
/-----BEGIN [^-]+-----[\s\S]*?-----END [^-]+-----/g
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Secret values in URL query or hash parameters (the first-owner setup link carries its token so). The logger still
|
|
17
|
+
* writes the link to the server's own output; only the console's copy is redacted.
|
|
18
|
+
*/
|
|
19
|
+
const SECRET_URL_PARAMETER_PATTERN = /([?&#](?:token|secret|password|code|key)=)[^&#\s"]+/gi;
|
|
20
|
+
/** How deep nested field values are searched; anything deeper is replaced whole. */
|
|
21
|
+
const MAXIMUM_FIELD_DEPTH = 8;
|
|
22
|
+
/**
|
|
23
|
+
* Removes secrets from a log message.
|
|
24
|
+
* @param message The message.
|
|
25
|
+
* @returns The message with every secret pattern replaced by "[redacted]".
|
|
26
|
+
*/
|
|
27
|
+
export function redactLogMessage(message) {
|
|
28
|
+
let redacted = message;
|
|
29
|
+
for (const pattern of SECRET_TEXT_PATTERNS)
|
|
30
|
+
redacted = redacted.replace(pattern, REDACTED_TEXT);
|
|
31
|
+
return redacted.replace(SECRET_URL_PARAMETER_PATTERN, "$1" + REDACTED_TEXT);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Removes secrets from log fields: a field whose name looks secret becomes "[redacted]" (except the DR §6.8 names),
|
|
35
|
+
* and text anywhere inside the values is redacted like a message.
|
|
36
|
+
* @param fields The fields.
|
|
37
|
+
* @returns A redacted copy.
|
|
38
|
+
*/
|
|
39
|
+
export function redactLogFields(fields) {
|
|
40
|
+
return redactObject(fields, 0);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Redacts one object's fields.
|
|
44
|
+
* @param fields The object.
|
|
45
|
+
* @param depth How deep it sits in the entry.
|
|
46
|
+
* @returns A redacted copy.
|
|
47
|
+
*/
|
|
48
|
+
function redactObject(fields, depth) {
|
|
49
|
+
const redacted = {};
|
|
50
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
51
|
+
const isSecretName = SECRET_FIELD_PATTERN.test(name) && !SAFE_FIELD_NAMES.has(name);
|
|
52
|
+
redacted[name] = isSecretName ? REDACTED_TEXT : redactValue(value, depth + 1);
|
|
53
|
+
}
|
|
54
|
+
return redacted;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Redacts one field value.
|
|
58
|
+
* @param value The value.
|
|
59
|
+
* @param depth How deep it sits in the entry.
|
|
60
|
+
* @returns A redacted copy.
|
|
61
|
+
*/
|
|
62
|
+
function redactValue(value, depth) {
|
|
63
|
+
if (typeof value === "string")
|
|
64
|
+
return redactLogMessage(value);
|
|
65
|
+
if (value == null || typeof value !== "object")
|
|
66
|
+
return value;
|
|
67
|
+
if (depth > MAXIMUM_FIELD_DEPTH)
|
|
68
|
+
return REDACTED_TEXT;
|
|
69
|
+
if (Array.isArray(value))
|
|
70
|
+
return value.map((item) => redactValue(item, depth + 1));
|
|
71
|
+
return redactObject(value, depth);
|
|
72
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type AdminApiRouter } from "./AdminApiRouter.js";
|
|
2
|
+
import type { AdminLogLevel } from "./AdminApiTypes.js";
|
|
3
|
+
import type { AdminCoreServices, AdminServices } from "./AdminServices.js";
|
|
4
|
+
/** The settings document of a fleet-wide temporary level, which instances that start later apply. */
|
|
5
|
+
export declare const LOG_LEVEL_SETTINGS_ID: string;
|
|
6
|
+
/**
|
|
7
|
+
* A temporary log level on this instance (D21): it replaces the configured level until a timer puts the configured one
|
|
8
|
+
* back and logs "log level reverted".
|
|
9
|
+
*/
|
|
10
|
+
export declare class TemporaryLogLevel {
|
|
11
|
+
/** The shared core services. */
|
|
12
|
+
private readonly services;
|
|
13
|
+
/** The pending revert, or null. */
|
|
14
|
+
private revertTimer;
|
|
15
|
+
/**
|
|
16
|
+
* Prepares the control; nothing changes until set() or applyStored().
|
|
17
|
+
* @param services The shared core services.
|
|
18
|
+
*/
|
|
19
|
+
constructor(services: AdminCoreServices);
|
|
20
|
+
/**
|
|
21
|
+
* Sets a temporary level, replacing any earlier one.
|
|
22
|
+
* @param level The level.
|
|
23
|
+
* @param revertAfterMinutes Minutes until the configured level comes back (more than 0).
|
|
24
|
+
*/
|
|
25
|
+
set(level: AdminLogLevel, revertAfterMinutes: number): void;
|
|
26
|
+
/** Applies a stored fleet-wide level that has not expired yet (an instance that starts during one). */
|
|
27
|
+
applyStored(): Promise<void>;
|
|
28
|
+
/** Cancels the pending revert (the console stops); the level stays as it is. */
|
|
29
|
+
stop(): void;
|
|
30
|
+
/** Puts the configured level back and logs it. */
|
|
31
|
+
private revert;
|
|
32
|
+
/** Clears the pending revert timer. */
|
|
33
|
+
private cancelRevert;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Registers the logs, logs/download and logs/level routes.
|
|
37
|
+
* @param router The admin API router.
|
|
38
|
+
* @param services The shared services.
|
|
39
|
+
*/
|
|
40
|
+
export declare function registerLogRoutes(router: AdminApiRouter, services: AdminServices): void;
|
|
41
|
+
/**
|
|
42
|
+
* Registers this instance's readLogs and setLogLevel fleet commands (R15).
|
|
43
|
+
* @param services The shared services.
|
|
44
|
+
*/
|
|
45
|
+
export declare function registerLogCommands(services: AdminServices): void;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// The log routes (game-server-admin-console DR §6.11 logs rows, D21): live tail and search across the fleet with a
|
|
2
|
+
// per-instance cursor, the JSON-lines download and the temporary log level, plus this instance's readLogs and
|
|
3
|
+
// setLogLevel fleet commands (R15) and the timer that puts a temporary level back.
|
|
4
|
+
import { getLogger } from "../GameServerLogger.js";
|
|
5
|
+
import { AdminRequestError } from "./AdminApiRouter.js";
|
|
6
|
+
import { DEFAULT_LOG_LEVEL_REVERT_MINUTES } from "./AdminConstants.js";
|
|
7
|
+
import { bodyObject, numberField, stringField } from "./AdminRequestBodies.js";
|
|
8
|
+
import { toLogEntryView } from "./LogBuffer.js";
|
|
9
|
+
import { canViewAddresses } from "./SessionRoutes.js";
|
|
10
|
+
/** HTTP status of a successful read or change. */
|
|
11
|
+
const HTTP_OK = 200;
|
|
12
|
+
/** HTTP status of an invalid query or body. */
|
|
13
|
+
const HTTP_BAD_REQUEST = 400;
|
|
14
|
+
/** The settings document of a fleet-wide temporary level, which instances that start later apply. */
|
|
15
|
+
export const LOG_LEVEL_SETTINGS_ID = "logLevel";
|
|
16
|
+
/** The levels the API accepts. */
|
|
17
|
+
const LOG_LEVELS = new Set(["error", "warn", "info", "debug"]);
|
|
18
|
+
/** Entries one GET logs answers with when the query does not say. */
|
|
19
|
+
const DEFAULT_PAGE_LIMIT = 200;
|
|
20
|
+
/** The most entries one GET logs answers with. */
|
|
21
|
+
const MAXIMUM_PAGE_LIMIT = 1000;
|
|
22
|
+
/** The scope value of the whole fleet. */
|
|
23
|
+
const FLEET_SCOPE = "fleet";
|
|
24
|
+
/** Milliseconds per minute. */
|
|
25
|
+
const MILLISECONDS_PER_MINUTE = 60000;
|
|
26
|
+
/** The download's content type (one JSON object per line). */
|
|
27
|
+
const JSON_LINES_CONTENT_TYPE = "application/x-ndjson";
|
|
28
|
+
/** The download's file name. */
|
|
29
|
+
const DOWNLOAD_FILE_NAME = "logs.jsonl";
|
|
30
|
+
/**
|
|
31
|
+
* A temporary log level on this instance (D21): it replaces the configured level until a timer puts the configured one
|
|
32
|
+
* back and logs "log level reverted".
|
|
33
|
+
*/
|
|
34
|
+
export class TemporaryLogLevel {
|
|
35
|
+
/** The shared core services. */
|
|
36
|
+
services;
|
|
37
|
+
/** The pending revert, or null. */
|
|
38
|
+
revertTimer = null;
|
|
39
|
+
/**
|
|
40
|
+
* Prepares the control; nothing changes until set() or applyStored().
|
|
41
|
+
* @param services The shared core services.
|
|
42
|
+
*/
|
|
43
|
+
constructor(services) {
|
|
44
|
+
this.services = services;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Sets a temporary level, replacing any earlier one.
|
|
48
|
+
* @param level The level.
|
|
49
|
+
* @param revertAfterMinutes Minutes until the configured level comes back (more than 0).
|
|
50
|
+
*/
|
|
51
|
+
set(level, revertAfterMinutes) {
|
|
52
|
+
this.cancelRevert();
|
|
53
|
+
getLogger().setLevel(level);
|
|
54
|
+
this.revertTimer = this.services.clock.setTimeout(() => this.revert(), revertAfterMinutes * MILLISECONDS_PER_MINUTE);
|
|
55
|
+
}
|
|
56
|
+
/** Applies a stored fleet-wide level that has not expired yet (an instance that starts during one). */
|
|
57
|
+
async applyStored() {
|
|
58
|
+
const stored = await this.services.store.getDocument("settings", LOG_LEVEL_SETTINGS_ID);
|
|
59
|
+
if (stored == null)
|
|
60
|
+
return;
|
|
61
|
+
const remainingMilliseconds = Date.parse(stored.value.until) - this.services.clock.now();
|
|
62
|
+
if (!(remainingMilliseconds > 0))
|
|
63
|
+
return;
|
|
64
|
+
this.cancelRevert();
|
|
65
|
+
getLogger().setLevel(stored.value.level);
|
|
66
|
+
this.revertTimer = this.services.clock.setTimeout(() => this.revert(), remainingMilliseconds);
|
|
67
|
+
}
|
|
68
|
+
/** Cancels the pending revert (the console stops); the level stays as it is. */
|
|
69
|
+
stop() {
|
|
70
|
+
this.cancelRevert();
|
|
71
|
+
}
|
|
72
|
+
/** Puts the configured level back and logs it. */
|
|
73
|
+
revert() {
|
|
74
|
+
this.revertTimer = null;
|
|
75
|
+
getLogger().setLevel(this.services.state.config.logLevel);
|
|
76
|
+
getLogger().info("log level reverted", { level: this.services.state.config.logLevel });
|
|
77
|
+
}
|
|
78
|
+
/** Clears the pending revert timer. */
|
|
79
|
+
cancelRevert() {
|
|
80
|
+
if (this.revertTimer != null)
|
|
81
|
+
this.services.clock.clearTimeout(this.revertTimer);
|
|
82
|
+
this.revertTimer = null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Registers the logs, logs/download and logs/level routes.
|
|
87
|
+
* @param router The admin API router.
|
|
88
|
+
* @param services The shared services.
|
|
89
|
+
*/
|
|
90
|
+
export function registerLogRoutes(router, services) {
|
|
91
|
+
const routes = [
|
|
92
|
+
{ method: "GET", pattern: "logs", permission: "logs.view", requiresStepUp: false, auditAction: null, handler: async (request) => ({ status: HTTP_OK, body: await readLogPage(services, request, MAXIMUM_PAGE_LIMIT) }) },
|
|
93
|
+
{ method: "GET", pattern: "logs/download", permission: "logs.view", requiresStepUp: false, auditAction: "logs.download", handler: async (request) => downloadLogs(services, request) },
|
|
94
|
+
{ method: "PUT", pattern: "logs/level", permission: "logs.manage", requiresStepUp: false, auditAction: "logs.level", handler: async (request) => setLogLevel(services, request) }
|
|
95
|
+
];
|
|
96
|
+
for (const route of routes)
|
|
97
|
+
router.register(route);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Registers this instance's readLogs and setLogLevel fleet commands (R15).
|
|
101
|
+
* @param services The shared services.
|
|
102
|
+
*/
|
|
103
|
+
export function registerLogCommands(services) {
|
|
104
|
+
services.fleet.handle("readLogs", async (command) => {
|
|
105
|
+
const entries = services.state.logBuffer?.read(command.filter, command.afterSequence, command.limit) ?? [];
|
|
106
|
+
return entries.map((entry) => toLogEntryView(entry, services.fleet.localProcessId, true));
|
|
107
|
+
});
|
|
108
|
+
services.fleet.handle("setLogLevel", async (command) => {
|
|
109
|
+
services.temporaryLogLevel.set(command.level, command.revertAfterMinutes);
|
|
110
|
+
return null;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* GET logs: the matching entries of one instance or of the fleet, merged by time, with the cursor of the next read.
|
|
115
|
+
* @param services The shared services.
|
|
116
|
+
* @param request The request.
|
|
117
|
+
* @param maximumLimit The most entries the query may ask for.
|
|
118
|
+
* @returns The page.
|
|
119
|
+
*/
|
|
120
|
+
async function readLogPage(services, request, maximumLimit) {
|
|
121
|
+
const filter = logFilterOf(request.query);
|
|
122
|
+
const cursor = cursorOf(request.query.get("after"));
|
|
123
|
+
const limit = limitOf(request.query.get("limit"), maximumLimit);
|
|
124
|
+
const processId = request.query.get("processId") ?? FLEET_SCOPE;
|
|
125
|
+
const entries = (processId === FLEET_SCOPE || processId === "")
|
|
126
|
+
? await readFleetLogs(services, filter, cursor, limit)
|
|
127
|
+
: await services.fleet.runOn(processId, "readLogs", { filter: filter, afterSequence: cursor[processId] ?? 0, limit: limit });
|
|
128
|
+
entries.sort(compareEntries);
|
|
129
|
+
// With a cursor the oldest new entries come first, so a tail misses nothing; a first read shows the newest.
|
|
130
|
+
const hasCursor = Object.keys(cursor).length > 0;
|
|
131
|
+
const kept = hasCursor ? entries.slice(0, limit) : entries.slice(Math.max(0, entries.length - limit));
|
|
132
|
+
const includeAddresses = canViewAddresses(request);
|
|
133
|
+
return { entries: kept.map((entry) => toLogEntryView(entry, entry.processId, includeAddresses)), cursor: encodeCursor(advanceCursor(cursor, kept)) };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Reads every live instance's matching entries, each after its own cursor; instances that fail are left out.
|
|
137
|
+
* @param services The shared services.
|
|
138
|
+
* @param filter What to keep.
|
|
139
|
+
* @param cursor Each instance's newest sequence seen.
|
|
140
|
+
* @param limit The most entries per instance.
|
|
141
|
+
* @returns The entries, unsorted.
|
|
142
|
+
*/
|
|
143
|
+
async function readFleetLogs(services, filter, cursor, limit) {
|
|
144
|
+
const processIds = [services.fleet.localProcessId];
|
|
145
|
+
for (const instance of await services.fleet.listInstances())
|
|
146
|
+
if (instance.status === "up" && !processIds.includes(instance.processId))
|
|
147
|
+
processIds.push(instance.processId);
|
|
148
|
+
const pages = await Promise.all(processIds.map(async (processId) => {
|
|
149
|
+
try {
|
|
150
|
+
return await services.fleet.runOn(processId, "readLogs", { filter: filter, afterSequence: cursor[processId] ?? 0, limit: limit });
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
// An instance that does not answer is left out of this read; the next poll asks it again.
|
|
154
|
+
if (error instanceof AdminRequestError)
|
|
155
|
+
return [];
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}));
|
|
159
|
+
return pages.flat();
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* GET logs/download: the same read as GET logs, up to the whole buffer, as a JSON-lines file.
|
|
163
|
+
* @param services The shared services.
|
|
164
|
+
* @param request The request.
|
|
165
|
+
* @returns The download.
|
|
166
|
+
*/
|
|
167
|
+
async function downloadLogs(services, request) {
|
|
168
|
+
const page = await readLogPage(services, request, services.config.admin.logBufferEntries);
|
|
169
|
+
const text = page.entries.map((entry) => JSON.stringify(entry) + "\n").join("");
|
|
170
|
+
const bytes = new TextEncoder().encode(text);
|
|
171
|
+
return {
|
|
172
|
+
status: HTTP_OK,
|
|
173
|
+
body: null,
|
|
174
|
+
download: { bytes: bytes, contentType: JSON_LINES_CONTENT_TYPE, fileName: DOWNLOAD_FILE_NAME },
|
|
175
|
+
audit: { target: request.query.get("processId") ?? FLEET_SCOPE, parameters: { entries: page.entries.length, filter: Object.assign({}, logFilterOf(request.query)) } }
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* PUT logs/level: a temporary level on one instance or the fleet; a fleet level is also stored, so instances that
|
|
180
|
+
* start before it reverts apply it too.
|
|
181
|
+
* @param services The shared services.
|
|
182
|
+
* @param request The request.
|
|
183
|
+
* @returns ok.
|
|
184
|
+
* @throws AdminRequestError 400 for an unknown level or a revert time that is not more than 0.
|
|
185
|
+
*/
|
|
186
|
+
async function setLogLevel(services, request) {
|
|
187
|
+
const body = bodyObject(request.body);
|
|
188
|
+
const scope = stringField(body, "scope");
|
|
189
|
+
const level = levelOf(stringField(body, "level"));
|
|
190
|
+
const revertAfterMinutes = (body.revertAfterMinutes == null) ? DEFAULT_LOG_LEVEL_REVERT_MINUTES : numberField(body, "revertAfterMinutes");
|
|
191
|
+
if (!(revertAfterMinutes > 0))
|
|
192
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "revertAfterMinutes must be more than 0");
|
|
193
|
+
const command = { level: level, revertAfterMinutes: revertAfterMinutes };
|
|
194
|
+
if (scope === FLEET_SCOPE) {
|
|
195
|
+
const until = new Date(services.clock.now() + revertAfterMinutes * MILLISECONDS_PER_MINUTE).toISOString();
|
|
196
|
+
await services.store.putDocument("settings", LOG_LEVEL_SETTINGS_ID, { level: level, until: until }, null);
|
|
197
|
+
const outcome = await services.fleet.runOnAll("setLogLevel", command);
|
|
198
|
+
return { status: HTTP_OK, body: { ok: true }, audit: { target: FLEET_SCOPE, parameters: { level: level, revertAfterMinutes: revertAfterMinutes, failedInstances: outcome.failures.length } } };
|
|
199
|
+
}
|
|
200
|
+
await services.fleet.runOn(scope, "setLogLevel", command);
|
|
201
|
+
return { status: HTTP_OK, body: { ok: true }, audit: { target: scope, parameters: { level: level, revertAfterMinutes: revertAfterMinutes } } };
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The filter of a logs query (level, roomId, sessionName, playerId, userId, text).
|
|
205
|
+
* @param query The query.
|
|
206
|
+
* @returns The filter.
|
|
207
|
+
* @throws AdminRequestError 400 for an unknown level or a playerId that is not a whole number.
|
|
208
|
+
*/
|
|
209
|
+
function logFilterOf(query) {
|
|
210
|
+
const filter = {};
|
|
211
|
+
const level = query.get("level");
|
|
212
|
+
if (level != null && level !== "")
|
|
213
|
+
filter.level = levelOf(level);
|
|
214
|
+
const playerId = query.get("playerId");
|
|
215
|
+
if (playerId != null && playerId !== "") {
|
|
216
|
+
const parsedPlayerId = Number(playerId);
|
|
217
|
+
if (!Number.isInteger(parsedPlayerId))
|
|
218
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "playerId must be a whole number");
|
|
219
|
+
filter.playerId = parsedPlayerId;
|
|
220
|
+
}
|
|
221
|
+
for (const field of ["roomId", "sessionName", "userId", "text"]) {
|
|
222
|
+
const value = query.get(field);
|
|
223
|
+
if (value != null && value !== "")
|
|
224
|
+
filter[field] = value;
|
|
225
|
+
}
|
|
226
|
+
return filter;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* A level name of a query or body.
|
|
230
|
+
* @param text The text.
|
|
231
|
+
* @returns The level.
|
|
232
|
+
* @throws AdminRequestError 400 for anything but error, warn, info or debug.
|
|
233
|
+
*/
|
|
234
|
+
function levelOf(text) {
|
|
235
|
+
if (!LOG_LEVELS.has(text))
|
|
236
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "level must be error, warn, info or debug");
|
|
237
|
+
return text;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* The entry limit of a query.
|
|
241
|
+
* @param text The limit query value, or null.
|
|
242
|
+
* @param maximumLimit The largest limit allowed.
|
|
243
|
+
* @returns The limit (DEFAULT_PAGE_LIMIT when missing, capped at maximumLimit).
|
|
244
|
+
* @throws AdminRequestError 400 when it is not a whole number of at least 1.
|
|
245
|
+
*/
|
|
246
|
+
function limitOf(text, maximumLimit) {
|
|
247
|
+
if (text == null || text === "")
|
|
248
|
+
return Math.min(DEFAULT_PAGE_LIMIT, maximumLimit);
|
|
249
|
+
const limit = Number(text);
|
|
250
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
251
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "limit must be a whole number of at least 1");
|
|
252
|
+
return Math.min(limit, maximumLimit);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Decodes a GET logs cursor.
|
|
256
|
+
* @param text The after query value, or null.
|
|
257
|
+
* @returns Each instance's newest sequence seen (empty for a first read).
|
|
258
|
+
* @throws AdminRequestError 400 when it is not a cursor this API made.
|
|
259
|
+
*/
|
|
260
|
+
function cursorOf(text) {
|
|
261
|
+
if (text == null || text === "")
|
|
262
|
+
return {};
|
|
263
|
+
let parsed;
|
|
264
|
+
try {
|
|
265
|
+
parsed = JSON.parse(Buffer.from(text, "base64url").toString("utf8"));
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
parsed = null;
|
|
269
|
+
}
|
|
270
|
+
const isCursor = parsed != null && typeof parsed === "object" && !Array.isArray(parsed) && Object.values(parsed).every((value) => Number.isInteger(value));
|
|
271
|
+
if (!isCursor)
|
|
272
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "after must be the cursor of an earlier answer");
|
|
273
|
+
return parsed;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Encodes a GET logs cursor.
|
|
277
|
+
* @param cursor Each instance's newest sequence seen.
|
|
278
|
+
* @returns The base64url JSON text.
|
|
279
|
+
*/
|
|
280
|
+
function encodeCursor(cursor) {
|
|
281
|
+
return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Moves a cursor past the entries a reader got.
|
|
285
|
+
* @param cursor The cursor of the read.
|
|
286
|
+
* @param entries The entries returned.
|
|
287
|
+
* @returns The new cursor.
|
|
288
|
+
*/
|
|
289
|
+
function advanceCursor(cursor, entries) {
|
|
290
|
+
const advanced = Object.assign({}, cursor);
|
|
291
|
+
for (const entry of entries)
|
|
292
|
+
advanced[entry.processId] = Math.max(advanced[entry.processId] ?? 0, entry.sequence);
|
|
293
|
+
return advanced;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Orders entries by time, then instance, then sequence.
|
|
297
|
+
* @param left The first entry.
|
|
298
|
+
* @param right The second entry.
|
|
299
|
+
* @returns Negative, zero or positive.
|
|
300
|
+
*/
|
|
301
|
+
function compareEntries(left, right) {
|
|
302
|
+
if (left.time !== right.time)
|
|
303
|
+
return left.time.localeCompare(right.time);
|
|
304
|
+
if (left.processId !== right.processId)
|
|
305
|
+
return left.processId.localeCompare(right.processId);
|
|
306
|
+
return left.sequence - right.sequence;
|
|
307
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { GameServerStateValues } from "../GameServerState.js";
|
|
2
|
+
import type { MaintenanceSettings, MaintenanceState } from "./AdminApiTypes.js";
|
|
3
|
+
import type { AdminStore } from "./AdminStore.js";
|
|
4
|
+
/** The settings document that holds the fleet maintenance (collection "settings"). */
|
|
5
|
+
export declare const MAINTENANCE_SETTINGS_ID: string;
|
|
6
|
+
/** Decides which maintenance is in force on this instance and publishes it to the server state. */
|
|
7
|
+
export declare class MaintenanceMode {
|
|
8
|
+
/** The server state whose maintenance field this class owns. */
|
|
9
|
+
private readonly state;
|
|
10
|
+
/** The stored fleet setting while it is enabled, else null. */
|
|
11
|
+
private fleetSetting;
|
|
12
|
+
/** Maintenance set for this instance only, else null. */
|
|
13
|
+
private instanceState;
|
|
14
|
+
/**
|
|
15
|
+
* Starts with no maintenance in force.
|
|
16
|
+
* @param state The server state the join gate reads.
|
|
17
|
+
*/
|
|
18
|
+
constructor(state: GameServerStateValues);
|
|
19
|
+
/**
|
|
20
|
+
* Applies the stored fleet setting; a disabled or missing setting ends fleet maintenance.
|
|
21
|
+
* @param settings The setting, or null when the document does not exist.
|
|
22
|
+
*/
|
|
23
|
+
applyFleetSetting(settings: MaintenanceSettings | null): void;
|
|
24
|
+
/**
|
|
25
|
+
* Sets or clears this instance's own maintenance. An enabled fleet setting still wins over a clear.
|
|
26
|
+
* @param maintenance The maintenance, or null to clear it.
|
|
27
|
+
*/
|
|
28
|
+
setInstance(maintenance: MaintenanceState | null): void;
|
|
29
|
+
/**
|
|
30
|
+
* The maintenance in force: the enabled fleet setting, else this instance's own, else none.
|
|
31
|
+
* @returns The maintenance, or null.
|
|
32
|
+
*/
|
|
33
|
+
current(): MaintenanceState | null;
|
|
34
|
+
/**
|
|
35
|
+
* Reads the stored fleet setting again and applies it (at start, and on every "settings" change announcement).
|
|
36
|
+
* @param store The admin data store.
|
|
37
|
+
*/
|
|
38
|
+
reloadFleetSetting(store: AdminStore): Promise<void>;
|
|
39
|
+
/** Puts the maintenance in force where the join gate and readiness read it. */
|
|
40
|
+
private publish;
|
|
41
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Maintenance mode of one instance (game-server-admin-console D14, DR §7.6): the stored fleet setting every instance
|
|
2
|
+
// applies, and a per-instance state that lives in memory until a restart. The join gate reads the result from
|
|
3
|
+
// state.maintenance.
|
|
4
|
+
/** The settings document that holds the fleet maintenance (collection "settings"). */
|
|
5
|
+
export const MAINTENANCE_SETTINGS_ID = "maintenance";
|
|
6
|
+
/** Decides which maintenance is in force on this instance and publishes it to the server state. */
|
|
7
|
+
export class MaintenanceMode {
|
|
8
|
+
/** The server state whose maintenance field this class owns. */
|
|
9
|
+
state;
|
|
10
|
+
/** The stored fleet setting while it is enabled, else null. */
|
|
11
|
+
fleetSetting = null;
|
|
12
|
+
/** Maintenance set for this instance only, else null. */
|
|
13
|
+
instanceState = null;
|
|
14
|
+
/**
|
|
15
|
+
* Starts with no maintenance in force.
|
|
16
|
+
* @param state The server state the join gate reads.
|
|
17
|
+
*/
|
|
18
|
+
constructor(state) {
|
|
19
|
+
this.state = state;
|
|
20
|
+
this.publish();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Applies the stored fleet setting; a disabled or missing setting ends fleet maintenance.
|
|
24
|
+
* @param settings The setting, or null when the document does not exist.
|
|
25
|
+
*/
|
|
26
|
+
applyFleetSetting(settings) {
|
|
27
|
+
this.fleetSetting = (settings != null && settings.enabled) ? settings : null;
|
|
28
|
+
this.publish();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Sets or clears this instance's own maintenance. An enabled fleet setting still wins over a clear.
|
|
32
|
+
* @param maintenance The maintenance, or null to clear it.
|
|
33
|
+
*/
|
|
34
|
+
setInstance(maintenance) {
|
|
35
|
+
this.instanceState = (maintenance != null) ? Object.assign({}, maintenance, { scope: "instance" }) : null;
|
|
36
|
+
this.publish();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The maintenance in force: the enabled fleet setting, else this instance's own, else none.
|
|
40
|
+
* @returns The maintenance, or null.
|
|
41
|
+
*/
|
|
42
|
+
current() {
|
|
43
|
+
if (this.fleetSetting != null)
|
|
44
|
+
return fleetMaintenanceOf(this.fleetSetting);
|
|
45
|
+
return this.instanceState;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Reads the stored fleet setting again and applies it (at start, and on every "settings" change announcement).
|
|
49
|
+
* @param store The admin data store.
|
|
50
|
+
*/
|
|
51
|
+
async reloadFleetSetting(store) {
|
|
52
|
+
const stored = await store.getDocument("settings", MAINTENANCE_SETTINGS_ID);
|
|
53
|
+
this.applyFleetSetting(stored?.value ?? null);
|
|
54
|
+
}
|
|
55
|
+
/** Puts the maintenance in force where the join gate and readiness read it. */
|
|
56
|
+
publish() {
|
|
57
|
+
this.state.maintenance = this.current();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The maintenance state of an enabled fleet setting.
|
|
62
|
+
* @param settings The stored setting.
|
|
63
|
+
* @returns The state, scope "fleet".
|
|
64
|
+
*/
|
|
65
|
+
function fleetMaintenanceOf(settings) {
|
|
66
|
+
return {
|
|
67
|
+
message: settings.message,
|
|
68
|
+
allowUserIds: settings.allowUserIds.slice(),
|
|
69
|
+
allowAddresses: settings.allowAddresses.slice(),
|
|
70
|
+
leaveReadiness: settings.leaveReadiness,
|
|
71
|
+
scope: "fleet",
|
|
72
|
+
endsAt: null
|
|
73
|
+
};
|
|
74
|
+
}
|