@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,351 @@
|
|
|
1
|
+
// `babylon-game-server admin <command>`: account recovery and key management directly on the admin data store
|
|
2
|
+
// (game-server-admin-console T3, DR §10). Every change is audited as the actor "cli" with processId "cli".
|
|
3
|
+
import { Redis } from "ioredis";
|
|
4
|
+
import { GameServerConfigError, loadGameServerConfig } from "../GameServerConfig.js";
|
|
5
|
+
import { AdminAccounts } from "./AdminAccounts.js";
|
|
6
|
+
import { AdminRequestError } from "./AdminApiRouter.js";
|
|
7
|
+
import { AdminAudit } from "./AdminAudit.js";
|
|
8
|
+
import { SYSTEM_CLOCK } from "./AdminClock.js";
|
|
9
|
+
import { ADMIN_PERMISSIONS, isAdminPermission } from "./AdminPermissions.js";
|
|
10
|
+
import { openAdminStore } from "./AdminStore.js";
|
|
11
|
+
import { ApiKeys } from "./ApiKeys.js";
|
|
12
|
+
import { ConsoleSessions } from "./ConsoleSessions.js";
|
|
13
|
+
/** Exit code of a usage or config problem. */
|
|
14
|
+
const USAGE_EXIT_CODE = 2;
|
|
15
|
+
/** HTTP-style status of an unknown account (AdminRequestError). */
|
|
16
|
+
const HTTP_NOT_FOUND = 404;
|
|
17
|
+
/** Exit code of any other failure. */
|
|
18
|
+
const FAILURE_EXIT_CODE = 1;
|
|
19
|
+
/** The flags passed on to the config loader; every other flag belongs to the command. */
|
|
20
|
+
const CONFIG_FLAGS = new Set(["--config", "--redis-url", "--admin-data-dir"]);
|
|
21
|
+
/** Prefix of a flag. */
|
|
22
|
+
const FLAG_PREFIX = "--";
|
|
23
|
+
/** The process id CLI audit records carry (R32). */
|
|
24
|
+
const CLI_PROCESS_ID = "cli";
|
|
25
|
+
/** Milliseconds per day. */
|
|
26
|
+
const MILLISECONDS_PER_DAY = 86400000;
|
|
27
|
+
/** The character a terminal sends for Enter, and its alternatives. */
|
|
28
|
+
const ENTER_CHARACTERS = new Set(["\r", "\n", "\u0004"]);
|
|
29
|
+
/** The character a terminal sends for Ctrl-C. */
|
|
30
|
+
const INTERRUPT_CHARACTER = "\u0003";
|
|
31
|
+
/** The characters a terminal sends for Backspace. */
|
|
32
|
+
const BACKSPACE_CHARACTERS = new Set(["\u007f", "\b"]);
|
|
33
|
+
/** The help text of the admin command. */
|
|
34
|
+
const USAGE_TEXT = [
|
|
35
|
+
"usage: babylon-game-server admin <command> [--config <file>] [--redis-url <url>] [--admin-data-dir <folder>]",
|
|
36
|
+
" list-users",
|
|
37
|
+
" create-owner --username <name> [--display-name <name>] [--password-env <VARIABLE>]",
|
|
38
|
+
" reset-password --username <name> [--password-env <VARIABLE>]",
|
|
39
|
+
" reset-second-factor --username <name>",
|
|
40
|
+
" unlock --username <name>",
|
|
41
|
+
" end-sessions --username <name>",
|
|
42
|
+
" create-api-key --name <name> --permissions <comma list> [--expires-days <days>]",
|
|
43
|
+
" revoke-api-key --key-id <id>"
|
|
44
|
+
].join("\n");
|
|
45
|
+
/** A wrong command line: printed with the usage and exit code 2. */
|
|
46
|
+
class AdminUsageError extends Error {
|
|
47
|
+
/**
|
|
48
|
+
* Creates the error.
|
|
49
|
+
* @param message What is wrong.
|
|
50
|
+
*/
|
|
51
|
+
constructor(message) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "AdminUsageError";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** The commands by name. */
|
|
57
|
+
const COMMANDS = {
|
|
58
|
+
"list-users": listUsers,
|
|
59
|
+
"create-owner": createOwner,
|
|
60
|
+
"reset-password": resetPassword,
|
|
61
|
+
"reset-second-factor": resetSecondFactor,
|
|
62
|
+
"unlock": unlockUser,
|
|
63
|
+
"end-sessions": endSessions,
|
|
64
|
+
"create-api-key": createApiKey,
|
|
65
|
+
"revoke-api-key": revokeApiKey
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Runs `babylon-game-server admin <command> …` and sets the exit code: 0 on success, 2 on a usage or config problem,
|
|
69
|
+
* 1 otherwise.
|
|
70
|
+
* @param argumentList The arguments after `admin`.
|
|
71
|
+
* @param environment The process environment.
|
|
72
|
+
*/
|
|
73
|
+
export async function runAdminCommand(argumentList, environment) {
|
|
74
|
+
let redisClient = null;
|
|
75
|
+
let store = null;
|
|
76
|
+
try {
|
|
77
|
+
const commandName = argumentList[0];
|
|
78
|
+
const command = (commandName != null) ? COMMANDS[commandName] : undefined;
|
|
79
|
+
if (command == null)
|
|
80
|
+
throw new AdminUsageError((commandName == null) ? "name a command" : "unknown command " + commandName);
|
|
81
|
+
const { configArguments, flags } = splitArguments(argumentList.slice(1));
|
|
82
|
+
const config = await loadGameServerConfig({ argv: configArguments, environment: environment, workingDirectory: process.cwd() });
|
|
83
|
+
redisClient = (config.redis.url != null) ? new Redis(config.redis.url) : null;
|
|
84
|
+
store = await openAdminStore(config, redisClient);
|
|
85
|
+
const sessions = new ConsoleSessions(store, SYSTEM_CLOCK, config.admin.sessionIdleMinutes, config.admin.sessionMaxHours);
|
|
86
|
+
const context = {
|
|
87
|
+
flags: flags,
|
|
88
|
+
environment: environment,
|
|
89
|
+
store: store,
|
|
90
|
+
accounts: new AdminAccounts(store, SYSTEM_CLOCK, sessions, false),
|
|
91
|
+
sessions: sessions,
|
|
92
|
+
apiKeys: new ApiKeys(store, SYSTEM_CLOCK),
|
|
93
|
+
audit: new AdminAudit(store, SYSTEM_CLOCK, () => CLI_PROCESS_ID)
|
|
94
|
+
};
|
|
95
|
+
await command(context);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
reportFailure(error);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
await store?.close();
|
|
102
|
+
if (redisClient != null)
|
|
103
|
+
await redisClient.quit();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Prints a failure and sets its exit code.
|
|
108
|
+
* @param error What went wrong.
|
|
109
|
+
*/
|
|
110
|
+
function reportFailure(error) {
|
|
111
|
+
if (error instanceof AdminUsageError) {
|
|
112
|
+
console.error(error.message + "\n" + USAGE_TEXT);
|
|
113
|
+
process.exitCode = USAGE_EXIT_CODE;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (error instanceof GameServerConfigError) {
|
|
117
|
+
console.error(error.message);
|
|
118
|
+
process.exitCode = USAGE_EXIT_CODE;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
console.error((error instanceof Error) ? error.message : String(error));
|
|
122
|
+
process.exitCode = FAILURE_EXIT_CODE;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Splits the arguments into the config flags (for loadGameServerConfig) and the command's own `--name value` flags.
|
|
126
|
+
* @param argumentList The arguments after the command name.
|
|
127
|
+
* @returns Both sets.
|
|
128
|
+
* @throws AdminUsageError for a flag without a value or a stray argument.
|
|
129
|
+
*/
|
|
130
|
+
function splitArguments(argumentList) {
|
|
131
|
+
const configArguments = [];
|
|
132
|
+
const flags = new Map();
|
|
133
|
+
for (let i = 0; i < argumentList.length; i += 2) {
|
|
134
|
+
const flag = argumentList[i];
|
|
135
|
+
const value = argumentList[i + 1];
|
|
136
|
+
if (!flag.startsWith(FLAG_PREFIX))
|
|
137
|
+
throw new AdminUsageError("unexpected argument " + flag);
|
|
138
|
+
if (value == null || value.startsWith(FLAG_PREFIX))
|
|
139
|
+
throw new AdminUsageError(flag + " needs a value");
|
|
140
|
+
if (CONFIG_FLAGS.has(flag))
|
|
141
|
+
configArguments.push(flag, value);
|
|
142
|
+
else
|
|
143
|
+
flags.set(flag.substring(FLAG_PREFIX.length), value);
|
|
144
|
+
}
|
|
145
|
+
return { configArguments: configArguments, flags: flags };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* A required flag of the command.
|
|
149
|
+
* @param context The command context.
|
|
150
|
+
* @param name The flag without dashes.
|
|
151
|
+
* @returns Its value.
|
|
152
|
+
* @throws AdminUsageError when it is missing.
|
|
153
|
+
*/
|
|
154
|
+
function requiredFlag(context, name) {
|
|
155
|
+
const value = context.flags.get(name);
|
|
156
|
+
if (value == null || value === "")
|
|
157
|
+
throw new AdminUsageError("--" + name + " is required");
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The account a --username flag names.
|
|
162
|
+
* @param context The command context.
|
|
163
|
+
* @returns The stored account.
|
|
164
|
+
* @throws AdminUsageError without --username; AdminRequestError when no such account exists.
|
|
165
|
+
*/
|
|
166
|
+
async function namedUser(context) {
|
|
167
|
+
const username = requiredFlag(context, "username");
|
|
168
|
+
const stored = await context.accounts.findByUsername(username);
|
|
169
|
+
if (stored == null)
|
|
170
|
+
throw new AdminRequestError(HTTP_NOT_FOUND, "no admin user named " + username);
|
|
171
|
+
return stored;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Audits a CLI action.
|
|
175
|
+
* @param context The command context.
|
|
176
|
+
* @param action The action.
|
|
177
|
+
* @param target What it acted on.
|
|
178
|
+
* @param parameters Its parameters (never secrets).
|
|
179
|
+
*/
|
|
180
|
+
async function auditCli(context, action, target, parameters = {}) {
|
|
181
|
+
const entry = { actor: { kind: "cli", id: "cli", name: "CLI" }, clientAddress: null, processId: CLI_PROCESS_ID, action: action, target: target, parameters: parameters, result: { ok: true, detail: null } };
|
|
182
|
+
await context.audit.record(entry);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The new password: from the variable --password-env names, else typed twice on the terminal with hidden input.
|
|
186
|
+
* @param context The command context.
|
|
187
|
+
* @returns The password.
|
|
188
|
+
* @throws AdminUsageError when neither source is available or the typed passwords differ.
|
|
189
|
+
*/
|
|
190
|
+
async function readNewPassword(context) {
|
|
191
|
+
const variableName = context.flags.get("password-env");
|
|
192
|
+
if (variableName != null) {
|
|
193
|
+
const fromEnvironment = context.environment[variableName];
|
|
194
|
+
if (fromEnvironment == null || fromEnvironment === "")
|
|
195
|
+
throw new AdminUsageError("the environment variable " + variableName + " is empty");
|
|
196
|
+
return fromEnvironment;
|
|
197
|
+
}
|
|
198
|
+
if (!process.stdin.isTTY)
|
|
199
|
+
throw new AdminUsageError("no terminal to type the password in: pass --password-env <VARIABLE>");
|
|
200
|
+
const first = await promptHidden("New password: ");
|
|
201
|
+
const second = await promptHidden("Repeat it: ");
|
|
202
|
+
if (first !== second)
|
|
203
|
+
throw new AdminUsageError("the two passwords differ");
|
|
204
|
+
return first;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Reads one line from the terminal without echoing it.
|
|
208
|
+
* @param question The prompt.
|
|
209
|
+
* @returns The typed text.
|
|
210
|
+
* @throws AdminUsageError when the user presses Ctrl-C.
|
|
211
|
+
*/
|
|
212
|
+
function promptHidden(question) {
|
|
213
|
+
return new Promise((resolve, reject) => {
|
|
214
|
+
const input = process.stdin;
|
|
215
|
+
let typed = "";
|
|
216
|
+
process.stdout.write(question);
|
|
217
|
+
input.setRawMode(true);
|
|
218
|
+
input.resume();
|
|
219
|
+
input.setEncoding("utf8");
|
|
220
|
+
const finish = (error) => {
|
|
221
|
+
input.setRawMode(false);
|
|
222
|
+
input.pause();
|
|
223
|
+
input.removeListener("data", onData);
|
|
224
|
+
process.stdout.write("\n");
|
|
225
|
+
if (error != null)
|
|
226
|
+
reject(error);
|
|
227
|
+
else
|
|
228
|
+
resolve(typed);
|
|
229
|
+
};
|
|
230
|
+
const onData = (chunk) => {
|
|
231
|
+
for (const character of chunk) {
|
|
232
|
+
if (ENTER_CHARACTERS.has(character)) {
|
|
233
|
+
finish(null);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (character === INTERRUPT_CHARACTER) {
|
|
237
|
+
finish(new AdminUsageError("cancelled"));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (BACKSPACE_CHARACTERS.has(character))
|
|
241
|
+
typed = typed.slice(0, -1);
|
|
242
|
+
else
|
|
243
|
+
typed += character;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
input.on("data", onData);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* `list-users`: one line per account.
|
|
251
|
+
* @param context The command context.
|
|
252
|
+
*/
|
|
253
|
+
async function listUsers(context) {
|
|
254
|
+
const users = await context.store.listDocuments("users");
|
|
255
|
+
users.sort((left, right) => left.value.username.localeCompare(right.value.username));
|
|
256
|
+
if (users.length === 0)
|
|
257
|
+
console.log("no admin users");
|
|
258
|
+
for (const stored of users) {
|
|
259
|
+
const user = stored.value;
|
|
260
|
+
const states = [];
|
|
261
|
+
if (user.disabled)
|
|
262
|
+
states.push("disabled");
|
|
263
|
+
if (context.accounts.isLocked(user))
|
|
264
|
+
states.push("locked");
|
|
265
|
+
states.push((user.totpSecret != null) ? "second factor enrolled" : "second factor not enrolled");
|
|
266
|
+
console.log(user.username + "\t" + user.role + "\t" + user.displayName + "\t" + states.join(", "));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* `create-owner`: an owner who enrols an authenticator at the first sign-in.
|
|
271
|
+
* @param context The command context.
|
|
272
|
+
*/
|
|
273
|
+
async function createOwner(context) {
|
|
274
|
+
const username = requiredFlag(context, "username");
|
|
275
|
+
const password = await readNewPassword(context);
|
|
276
|
+
const owner = await context.accounts.createUser({ username: username, displayName: context.flags.get("display-name") ?? "", email: null, role: "owner", password: password, oidcSubject: null });
|
|
277
|
+
await auditCli(context, "user.create", owner.username, { role: "owner" });
|
|
278
|
+
console.log("created owner " + owner.username + "; sign in at /game-server/admin/ to enrol an authenticator");
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* `reset-password`: sets a new password and ends the account's sessions.
|
|
282
|
+
* @param context The command context.
|
|
283
|
+
*/
|
|
284
|
+
async function resetPassword(context) {
|
|
285
|
+
const stored = await namedUser(context);
|
|
286
|
+
await context.accounts.setPassword(stored, await readNewPassword(context));
|
|
287
|
+
await context.sessions.endAllFor(stored.id);
|
|
288
|
+
await auditCli(context, "user.resetPassword", stored.value.username);
|
|
289
|
+
console.log("password reset for " + stored.value.username);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* `reset-second-factor`: the account enrols a new authenticator at its next sign-in.
|
|
293
|
+
* @param context The command context.
|
|
294
|
+
*/
|
|
295
|
+
async function resetSecondFactor(context) {
|
|
296
|
+
const stored = await namedUser(context);
|
|
297
|
+
await context.accounts.resetSecondFactor(stored.id);
|
|
298
|
+
await context.sessions.endAllFor(stored.id);
|
|
299
|
+
await auditCli(context, "user.resetSecondFactor", stored.value.username);
|
|
300
|
+
console.log("second factor reset for " + stored.value.username);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* `unlock`: clears a lockout.
|
|
304
|
+
* @param context The command context.
|
|
305
|
+
*/
|
|
306
|
+
async function unlockUser(context) {
|
|
307
|
+
const stored = await namedUser(context);
|
|
308
|
+
await context.accounts.unlock(stored.id);
|
|
309
|
+
await auditCli(context, "user.unlock", stored.value.username);
|
|
310
|
+
console.log("unlocked " + stored.value.username);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* `end-sessions`: signs the account out everywhere.
|
|
314
|
+
* @param context The command context.
|
|
315
|
+
*/
|
|
316
|
+
async function endSessions(context) {
|
|
317
|
+
const stored = await namedUser(context);
|
|
318
|
+
const ended = await context.sessions.endAllFor(stored.id);
|
|
319
|
+
await auditCli(context, "user.endSessions", stored.value.username, { ended: ended });
|
|
320
|
+
console.log("ended " + ended + " session(s) of " + stored.value.username);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* `create-api-key`: prints the secret once.
|
|
324
|
+
* @param context The command context.
|
|
325
|
+
* @throws AdminUsageError for an unknown permission or a bad expiry.
|
|
326
|
+
*/
|
|
327
|
+
async function createApiKey(context) {
|
|
328
|
+
const name = requiredFlag(context, "name");
|
|
329
|
+
const permissionNames = requiredFlag(context, "permissions").split(",").map((permission) => permission.trim()).filter((permission) => permission !== "");
|
|
330
|
+
const unknown = permissionNames.find((permission) => !isAdminPermission(permission));
|
|
331
|
+
if (unknown != null)
|
|
332
|
+
throw new AdminUsageError("unknown permission " + unknown + " (known: " + ADMIN_PERMISSIONS.join(", ") + ")");
|
|
333
|
+
const expiresDaysText = context.flags.get("expires-days");
|
|
334
|
+
const expiresDays = (expiresDaysText != null) ? Number(expiresDaysText) : null;
|
|
335
|
+
if (expiresDays != null && !(Number.isFinite(expiresDays) && expiresDays > 0))
|
|
336
|
+
throw new AdminUsageError("--expires-days must be a number of days above 0");
|
|
337
|
+
const expiresAt = (expiresDays != null) ? new Date(Date.now() + expiresDays * MILLISECONDS_PER_DAY).toISOString() : null;
|
|
338
|
+
const created = await context.apiKeys.create({ name: name, permissions: permissionNames, expiresAt: expiresAt, allowedNetworks: [] }, CLI_PROCESS_ID, new Set(ADMIN_PERMISSIONS));
|
|
339
|
+
await auditCli(context, "apiKey.create", created.record.name, { keyId: created.record.keyId, permissions: created.record.permissions });
|
|
340
|
+
console.log("key id: " + created.record.keyId);
|
|
341
|
+
console.log("secret (shown once): " + created.secret);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* `revoke-api-key`.
|
|
345
|
+
* @param context The command context.
|
|
346
|
+
*/
|
|
347
|
+
async function revokeApiKey(context) {
|
|
348
|
+
const revoked = await context.apiKeys.revoke(requiredFlag(context, "key-id"));
|
|
349
|
+
await auditCli(context, "apiKey.revoke", revoked.name, { keyId: revoked.keyId });
|
|
350
|
+
console.log("revoked key " + revoked.keyId);
|
|
351
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Time and timers, as the admin services use them. */
|
|
2
|
+
export interface AdminClock {
|
|
3
|
+
/**
|
|
4
|
+
* The current time.
|
|
5
|
+
* @returns Milliseconds since the Unix epoch.
|
|
6
|
+
*/
|
|
7
|
+
now(): number;
|
|
8
|
+
/**
|
|
9
|
+
* Runs a callback once after a delay.
|
|
10
|
+
* @param callback What to run.
|
|
11
|
+
* @param delayMs The delay, in milliseconds.
|
|
12
|
+
* @returns A handle for clearTimeout.
|
|
13
|
+
*/
|
|
14
|
+
setTimeout(callback: () => void, delayMs: number): unknown;
|
|
15
|
+
/**
|
|
16
|
+
* Cancels a setTimeout.
|
|
17
|
+
* @param handle The handle setTimeout returned.
|
|
18
|
+
*/
|
|
19
|
+
clearTimeout(handle: unknown): void;
|
|
20
|
+
/**
|
|
21
|
+
* Runs a callback repeatedly.
|
|
22
|
+
* @param callback What to run.
|
|
23
|
+
* @param intervalMs The interval, in milliseconds.
|
|
24
|
+
* @returns A handle for clearInterval.
|
|
25
|
+
*/
|
|
26
|
+
setInterval(callback: () => void, intervalMs: number): unknown;
|
|
27
|
+
/**
|
|
28
|
+
* Cancels a setInterval.
|
|
29
|
+
* @param handle The handle setInterval returned.
|
|
30
|
+
*/
|
|
31
|
+
clearInterval(handle: unknown): void;
|
|
32
|
+
}
|
|
33
|
+
/** GameServerOptions.adminTesting: values only tests replace (R10). */
|
|
34
|
+
export interface AdminTestingOptions {
|
|
35
|
+
/** The clock of every admin timer (default: the system clock). */
|
|
36
|
+
clock?: AdminClock;
|
|
37
|
+
/** Milliseconds between fleet heartbeats (default FLEET_HEARTBEAT_INTERVAL_MS). */
|
|
38
|
+
heartbeatIntervalMs?: number;
|
|
39
|
+
/** Milliseconds between metrics samples (default METRICS_SAMPLE_INTERVAL_MS). */
|
|
40
|
+
metricsSampleIntervalMs?: number;
|
|
41
|
+
/** Milliseconds between leader ticks (default LEADER_TICK_MS). */
|
|
42
|
+
leaderTickMs?: number;
|
|
43
|
+
/** The PagerDuty Events v2 URL alert channels post to (default the real one). */
|
|
44
|
+
pagerDutyUrl?: string;
|
|
45
|
+
/** Called after each Colyseus Cloud refresh pass of the leader, with the clock time it ran at (colyseus-cloud-console D17). */
|
|
46
|
+
colyseusCloudRefreshed?: (nowMilliseconds: number) => void;
|
|
47
|
+
/** Milliseconds between leaderboard scheduler ticks (default LEADERBOARD_SCHEDULER_TICK_MS). */
|
|
48
|
+
leaderboardSchedulerTickMs?: number;
|
|
49
|
+
}
|
|
50
|
+
/** The real clock: Date.now and the global timers. Its timers never keep the process alive. */
|
|
51
|
+
export declare const SYSTEM_CLOCK: AdminClock;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The clock every admin timer and every "now" goes through (game-server-admin-console DR §13 R10), so tests can move
|
|
2
|
+
// time with a manual clock instead of waiting.
|
|
3
|
+
/** The real clock: Date.now and the global timers. Its timers never keep the process alive. */
|
|
4
|
+
export const SYSTEM_CLOCK = {
|
|
5
|
+
now: () => Date.now(),
|
|
6
|
+
setTimeout: (callback, delayMs) => setTimeout(callback, delayMs).unref(),
|
|
7
|
+
clearTimeout: (handle) => clearTimeout(handle),
|
|
8
|
+
setInterval: (callback, intervalMs) => setInterval(callback, intervalMs).unref(),
|
|
9
|
+
clearInterval: (handle) => clearInterval(handle)
|
|
10
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type Endpoint } from "colyseus";
|
|
2
|
+
import type { AdminCoreServices } from "./AdminServices.js";
|
|
3
|
+
/** The web console and its API, as GameServer.ts uses them. */
|
|
4
|
+
export interface AdminConsole {
|
|
5
|
+
/**
|
|
6
|
+
* The router entries of the app and the API.
|
|
7
|
+
* @returns The endpoints, keyed for createRouter.
|
|
8
|
+
*/
|
|
9
|
+
routes(): Record<string, Endpoint>;
|
|
10
|
+
/**
|
|
11
|
+
* The router's onRequest hook: answers GET /game-server/admin with a 308 to /game-server/admin/. The router keeps
|
|
12
|
+
* both paths on one node and 404s whichever does not match the first route's trailing slash, so this redirect
|
|
13
|
+
* cannot be an endpoint of its own.
|
|
14
|
+
* @param request The incoming request.
|
|
15
|
+
* @returns The redirect, or undefined to let the router go on.
|
|
16
|
+
*/
|
|
17
|
+
redirectBarePath(request: Request): Response | undefined;
|
|
18
|
+
/** Starts the console's timers; call after listen. */
|
|
19
|
+
start(): Promise<void>;
|
|
20
|
+
/** Stops the console's timers; call before the server shuts down (R4). */
|
|
21
|
+
stop(): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Whether the API answers (R6): admin.console on and a user, a pending setup or admin.token.
|
|
24
|
+
* @returns True when it answers.
|
|
25
|
+
*/
|
|
26
|
+
isAvailable(): Promise<boolean>;
|
|
27
|
+
/**
|
|
28
|
+
* Appends a server event of this instance to the activity feed (the certificate reload, T14).
|
|
29
|
+
* @param text What happened.
|
|
30
|
+
*/
|
|
31
|
+
recordServerEvent(text: string): void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Builds the console of this process: its services, its static files (read into memory now) and its API routes. The
|
|
35
|
+
* console is started but not listening; GameServer.ts merges routes() into the router and calls start() after listen.
|
|
36
|
+
* @param core What GameServer.ts hands the console.
|
|
37
|
+
* @returns The console.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createAdminConsole(core: AdminCoreServices): Promise<AdminConsole>;
|