@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,4 @@
|
|
|
1
|
+
// The admin API contract (game-server-admin-console DR §6.11): every request and response body of
|
|
2
|
+
// /game-server/admin/api/*. This file has no imports (DR §13 R1), so the console app type-checks against it without
|
|
3
|
+
// server, Colyseus or Node types. Times are ISO 8601 strings and ids are strings unless a field says otherwise.
|
|
4
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { ActivityItemView, AuditRecordView, AuditVerifyView } from "./AdminApiTypes.js";
|
|
2
|
+
import type { AdminClock } from "./AdminClock.js";
|
|
3
|
+
import type { AdminStore } from "./AdminStore.js";
|
|
4
|
+
/** What a caller passes to record(): the record without its sequence, time and hashes. */
|
|
5
|
+
export type AuditEntry = Omit<AuditRecordView, "sequence" | "at" | "previousHash" | "hash">;
|
|
6
|
+
/** Audit filters (R32). */
|
|
7
|
+
export interface AuditQuery {
|
|
8
|
+
/** Actor name or id contains this (any case). */
|
|
9
|
+
actor?: string;
|
|
10
|
+
/** Action starts with this. */
|
|
11
|
+
action?: string;
|
|
12
|
+
/** Target contains this (any case). */
|
|
13
|
+
target?: string;
|
|
14
|
+
/** Only successes or only failures. */
|
|
15
|
+
result?: "ok" | "failed";
|
|
16
|
+
/** Records at or after this ISO time. */
|
|
17
|
+
from?: string;
|
|
18
|
+
/** Records at or before this ISO time. */
|
|
19
|
+
to?: string;
|
|
20
|
+
}
|
|
21
|
+
/** The audit log of the admin store. */
|
|
22
|
+
export declare class AdminAudit {
|
|
23
|
+
/** The admin store. */
|
|
24
|
+
private readonly store;
|
|
25
|
+
/** The clock of retention cutoffs. */
|
|
26
|
+
private readonly clock;
|
|
27
|
+
/** This instance's process id ("cli" for the CLI). */
|
|
28
|
+
private readonly processId;
|
|
29
|
+
/**
|
|
30
|
+
* Creates the audit log.
|
|
31
|
+
* @param store The admin store.
|
|
32
|
+
* @param clock The clock.
|
|
33
|
+
* @param processId This instance's process id.
|
|
34
|
+
*/
|
|
35
|
+
constructor(store: AdminStore, clock: AdminClock, processId: () => string);
|
|
36
|
+
/**
|
|
37
|
+
* Appends an audited action, linked to the newest record by its hash (D19), and an activity item for a success.
|
|
38
|
+
* @param entry The action.
|
|
39
|
+
* @returns The stored record.
|
|
40
|
+
*/
|
|
41
|
+
record(entry: AuditEntry): Promise<AuditRecordView>;
|
|
42
|
+
/**
|
|
43
|
+
* Records an action and logs instead of throwing when the store fails, so an audit problem never fails the action.
|
|
44
|
+
* @param entry The action.
|
|
45
|
+
*/
|
|
46
|
+
recordSafely(entry: AuditEntry): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Checks the hash chain from the oldest remaining record (D19).
|
|
49
|
+
* @returns ok, how many records were checked, and the first broken sequence.
|
|
50
|
+
*/
|
|
51
|
+
verifyChain(): Promise<AuditVerifyView>;
|
|
52
|
+
/**
|
|
53
|
+
* Removes records older than the retention (settings/auditRetention, default DEFAULT_AUDIT_RETENTION_DAYS) and audits
|
|
54
|
+
* the cut as the system actor.
|
|
55
|
+
* @returns How many records were removed.
|
|
56
|
+
*/
|
|
57
|
+
applyRetention(): Promise<number>;
|
|
58
|
+
/**
|
|
59
|
+
* The retention in days.
|
|
60
|
+
* @returns The stored setting, or DEFAULT_AUDIT_RETENTION_DAYS.
|
|
61
|
+
*/
|
|
62
|
+
retentionDays(): Promise<number>;
|
|
63
|
+
/**
|
|
64
|
+
* Stores a new retention.
|
|
65
|
+
* @param days Whole days, at least 1.
|
|
66
|
+
*/
|
|
67
|
+
setRetentionDays(days: number): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* One page of matching records, newest first.
|
|
70
|
+
* @param filters The filters.
|
|
71
|
+
* @param page The page, from 1.
|
|
72
|
+
* @returns The page's records and the number of matches.
|
|
73
|
+
*/
|
|
74
|
+
query(filters: AuditQuery, page: number): Promise<{
|
|
75
|
+
items: AuditRecordView[];
|
|
76
|
+
total: number;
|
|
77
|
+
}>;
|
|
78
|
+
/**
|
|
79
|
+
* Every matching record as a file, oldest first.
|
|
80
|
+
* @param format "csv" (CSV_COLUMNS) or "jsonl" (one record per line).
|
|
81
|
+
* @param filters The filters.
|
|
82
|
+
* @returns The file bytes.
|
|
83
|
+
*/
|
|
84
|
+
export(format: "csv" | "jsonl", filters: AuditQuery): Promise<Uint8Array<ArrayBuffer>>;
|
|
85
|
+
/**
|
|
86
|
+
* The newest activity items.
|
|
87
|
+
* @param limit How many.
|
|
88
|
+
* @returns The items, newest first.
|
|
89
|
+
*/
|
|
90
|
+
activity(limit: number): Promise<ActivityItemView[]>;
|
|
91
|
+
/**
|
|
92
|
+
* Appends the activity item of a successful action: "<actor> <action> <target>".
|
|
93
|
+
* @param record The audit record.
|
|
94
|
+
*/
|
|
95
|
+
private recordActivity;
|
|
96
|
+
/**
|
|
97
|
+
* The records that match the filters, oldest first.
|
|
98
|
+
* @param filters The filters.
|
|
99
|
+
* @returns The records.
|
|
100
|
+
*/
|
|
101
|
+
private matching;
|
|
102
|
+
/**
|
|
103
|
+
* Every audit record, oldest first, read in pages of RECORD_QUERY_MAX_LIMIT.
|
|
104
|
+
* @returns The records.
|
|
105
|
+
*/
|
|
106
|
+
private readAll;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* JSON with object keys sorted at every level, so the same record always hashes the same.
|
|
110
|
+
* @param value The value.
|
|
111
|
+
* @returns The canonical JSON.
|
|
112
|
+
*/
|
|
113
|
+
export declare function canonicalJson(value: unknown): string;
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// The admin audit log (game-server-admin-console D19, DR §8): an append-only stream whose records are linked by a
|
|
2
|
+
// SHA-256 hash chain, with retention, filtered queries, CSV/JSON-lines export and the activity feed.
|
|
3
|
+
import { getLogger } from "../GameServerLogger.js";
|
|
4
|
+
import { ADMIN_PAGE_SIZE, AUDIT_CHAIN_SEED, DEFAULT_AUDIT_RETENTION_DAYS } from "./AdminConstants.js";
|
|
5
|
+
import { sha256Hex } from "./AdminCrypto.js";
|
|
6
|
+
import { RECORD_QUERY_MAX_LIMIT } from "./AdminStore.js";
|
|
7
|
+
/** The settings document holding the retention in days. */
|
|
8
|
+
const RETENTION_SETTINGS_ID = "auditRetention";
|
|
9
|
+
/** Milliseconds per day. */
|
|
10
|
+
const MILLISECONDS_PER_DAY = 86400000;
|
|
11
|
+
/** Columns of the CSV export, in order. */
|
|
12
|
+
const CSV_COLUMNS = ["sequence", "at", "actorKind", "actorName", "clientAddress", "processId", "action", "target", "result", "detail"];
|
|
13
|
+
/** Characters that make a CSV field need quotes. */
|
|
14
|
+
const CSV_SPECIAL_CHARACTERS = /[",\r\n]/;
|
|
15
|
+
/** The audit log of the admin store. */
|
|
16
|
+
export class AdminAudit {
|
|
17
|
+
/** The admin store. */
|
|
18
|
+
store;
|
|
19
|
+
/** The clock of retention cutoffs. */
|
|
20
|
+
clock;
|
|
21
|
+
/** This instance's process id ("cli" for the CLI). */
|
|
22
|
+
processId;
|
|
23
|
+
/**
|
|
24
|
+
* Creates the audit log.
|
|
25
|
+
* @param store The admin store.
|
|
26
|
+
* @param clock The clock.
|
|
27
|
+
* @param processId This instance's process id.
|
|
28
|
+
*/
|
|
29
|
+
constructor(store, clock, processId) {
|
|
30
|
+
this.store = store;
|
|
31
|
+
this.clock = clock;
|
|
32
|
+
this.processId = processId;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Appends an audited action, linked to the newest record by its hash (D19), and an activity item for a success.
|
|
36
|
+
* @param entry The action.
|
|
37
|
+
* @returns The stored record.
|
|
38
|
+
*/
|
|
39
|
+
async record(entry) {
|
|
40
|
+
const stored = await this.store.appendChainedRecord("audit", (previous, sequence, at) => {
|
|
41
|
+
const previousHash = previous?.value.hash ?? AUDIT_CHAIN_SEED;
|
|
42
|
+
const unhashed = Object.assign({ sequence: sequence, at: at }, entry);
|
|
43
|
+
return Object.assign({}, unhashed, { previousHash: previousHash, hash: auditHash(previousHash, unhashed) });
|
|
44
|
+
});
|
|
45
|
+
if (entry.result.ok)
|
|
46
|
+
await this.recordActivity(stored.value);
|
|
47
|
+
return stored.value;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Records an action and logs instead of throwing when the store fails, so an audit problem never fails the action.
|
|
51
|
+
* @param entry The action.
|
|
52
|
+
*/
|
|
53
|
+
async recordSafely(entry) {
|
|
54
|
+
try {
|
|
55
|
+
await this.record(entry);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
getLogger().error("admin audit record failed", { action: entry.action, error: String(error) });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Checks the hash chain from the oldest remaining record (D19).
|
|
63
|
+
* @returns ok, how many records were checked, and the first broken sequence.
|
|
64
|
+
*/
|
|
65
|
+
async verifyChain() {
|
|
66
|
+
let checked = 0;
|
|
67
|
+
let expectedPrevious = null;
|
|
68
|
+
for (const record of await this.readAll()) {
|
|
69
|
+
const value = record.value;
|
|
70
|
+
const { previousHash, hash, ...unhashed } = value;
|
|
71
|
+
const linkBroken = expectedPrevious != null && previousHash !== expectedPrevious;
|
|
72
|
+
const hashBroken = auditHash(previousHash, unhashed) !== hash || value.sequence !== record.sequence;
|
|
73
|
+
if (linkBroken || hashBroken)
|
|
74
|
+
return { ok: false, checked: checked, brokenAtSequence: record.sequence };
|
|
75
|
+
expectedPrevious = hash;
|
|
76
|
+
checked++;
|
|
77
|
+
}
|
|
78
|
+
return { ok: true, checked: checked, brokenAtSequence: null };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Removes records older than the retention (settings/auditRetention, default DEFAULT_AUDIT_RETENTION_DAYS) and audits
|
|
82
|
+
* the cut as the system actor.
|
|
83
|
+
* @returns How many records were removed.
|
|
84
|
+
*/
|
|
85
|
+
async applyRetention() {
|
|
86
|
+
const days = await this.retentionDays();
|
|
87
|
+
const cutoff = new Date(this.clock.now() - days * MILLISECONDS_PER_DAY);
|
|
88
|
+
const removed = await this.store.trimRecordsBefore("audit", cutoff);
|
|
89
|
+
if (removed > 0) {
|
|
90
|
+
await this.record({ actor: { kind: "system", id: "system", name: "system" }, clientAddress: null, processId: this.processId(), action: "audit.retentionApplied", target: null, parameters: { days: days, removed: removed }, result: { ok: true, detail: null } });
|
|
91
|
+
}
|
|
92
|
+
return removed;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The retention in days.
|
|
96
|
+
* @returns The stored setting, or DEFAULT_AUDIT_RETENTION_DAYS.
|
|
97
|
+
*/
|
|
98
|
+
async retentionDays() {
|
|
99
|
+
const stored = await this.store.getDocument("settings", RETENTION_SETTINGS_ID);
|
|
100
|
+
return stored?.value.days ?? DEFAULT_AUDIT_RETENTION_DAYS;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Stores a new retention.
|
|
104
|
+
* @param days Whole days, at least 1.
|
|
105
|
+
*/
|
|
106
|
+
async setRetentionDays(days) {
|
|
107
|
+
await this.store.putDocument("settings", RETENTION_SETTINGS_ID, { days: days }, null);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* One page of matching records, newest first.
|
|
111
|
+
* @param filters The filters.
|
|
112
|
+
* @param page The page, from 1.
|
|
113
|
+
* @returns The page's records and the number of matches.
|
|
114
|
+
*/
|
|
115
|
+
async query(filters, page) {
|
|
116
|
+
const matching = (await this.matching(filters)).reverse();
|
|
117
|
+
const firstIndex = (Math.max(1, Math.floor(page)) - 1) * ADMIN_PAGE_SIZE;
|
|
118
|
+
return { items: matching.slice(firstIndex, firstIndex + ADMIN_PAGE_SIZE), total: matching.length };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Every matching record as a file, oldest first.
|
|
122
|
+
* @param format "csv" (CSV_COLUMNS) or "jsonl" (one record per line).
|
|
123
|
+
* @param filters The filters.
|
|
124
|
+
* @returns The file bytes.
|
|
125
|
+
*/
|
|
126
|
+
async export(format, filters) {
|
|
127
|
+
const records = await this.matching(filters);
|
|
128
|
+
const lines = (format === "csv")
|
|
129
|
+
? [CSV_COLUMNS.join(",")].concat(records.map(csvRow))
|
|
130
|
+
: records.map((record) => JSON.stringify(record));
|
|
131
|
+
return new TextEncoder().encode(lines.map((line) => line + "\n").join(""));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The newest activity items.
|
|
135
|
+
* @param limit How many.
|
|
136
|
+
* @returns The items, newest first.
|
|
137
|
+
*/
|
|
138
|
+
async activity(limit) {
|
|
139
|
+
const records = await this.store.readRecords("events", { limit: limit, newestFirst: true });
|
|
140
|
+
return records.map((record) => record.value);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Appends the activity item of a successful action: "<actor> <action> <target>".
|
|
144
|
+
* @param record The audit record.
|
|
145
|
+
*/
|
|
146
|
+
async recordActivity(record) {
|
|
147
|
+
const text = [record.actor.name, record.action, record.target ?? ""].join(" ").trim();
|
|
148
|
+
const item = { at: record.at, kind: "admin", text: text, processId: record.processId };
|
|
149
|
+
await this.store.appendRecord("events", item);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The records that match the filters, oldest first.
|
|
153
|
+
* @param filters The filters.
|
|
154
|
+
* @returns The records.
|
|
155
|
+
*/
|
|
156
|
+
async matching(filters) {
|
|
157
|
+
const records = await this.readAll();
|
|
158
|
+
return records.map((record) => record.value).filter((record) => matchesQuery(record, filters));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Every audit record, oldest first, read in pages of RECORD_QUERY_MAX_LIMIT.
|
|
162
|
+
* @returns The records.
|
|
163
|
+
*/
|
|
164
|
+
async readAll() {
|
|
165
|
+
const records = [];
|
|
166
|
+
let afterSequence = 0;
|
|
167
|
+
for (;;) {
|
|
168
|
+
const page = await this.store.readRecords("audit", { afterSequence: afterSequence, limit: RECORD_QUERY_MAX_LIMIT, newestFirst: false });
|
|
169
|
+
records.push(...page);
|
|
170
|
+
if (page.length < RECORD_QUERY_MAX_LIMIT)
|
|
171
|
+
return records;
|
|
172
|
+
afterSequence = page[page.length - 1].sequence;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The hash of an audit record: sha256(previousHash + canonicalJson(record without its hashes)) (DR §8).
|
|
178
|
+
* @param previousHash The previous record's hash (AUDIT_CHAIN_SEED for the first).
|
|
179
|
+
* @param unhashed The record without previousHash and hash.
|
|
180
|
+
* @returns Lower-case hex.
|
|
181
|
+
*/
|
|
182
|
+
function auditHash(previousHash, unhashed) {
|
|
183
|
+
const covered = {
|
|
184
|
+
sequence: unhashed.sequence,
|
|
185
|
+
at: unhashed.at,
|
|
186
|
+
actor: unhashed.actor,
|
|
187
|
+
clientAddress: unhashed.clientAddress,
|
|
188
|
+
processId: unhashed.processId,
|
|
189
|
+
action: unhashed.action,
|
|
190
|
+
target: unhashed.target,
|
|
191
|
+
parameters: unhashed.parameters,
|
|
192
|
+
result: unhashed.result
|
|
193
|
+
};
|
|
194
|
+
return sha256Hex(previousHash + canonicalJson(covered));
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* JSON with object keys sorted at every level, so the same record always hashes the same.
|
|
198
|
+
* @param value The value.
|
|
199
|
+
* @returns The canonical JSON.
|
|
200
|
+
*/
|
|
201
|
+
export function canonicalJson(value) {
|
|
202
|
+
if (Array.isArray(value))
|
|
203
|
+
return "[" + value.map(canonicalJson).join(",") + "]";
|
|
204
|
+
if (value !== null && typeof value === "object") {
|
|
205
|
+
const entries = Object.entries(value).filter(([, item]) => item !== undefined);
|
|
206
|
+
entries.sort(([left], [right]) => compareCodePoints(left, right));
|
|
207
|
+
return "{" + entries.map(([key, item]) => JSON.stringify(key) + ":" + canonicalJson(item)).join(",") + "}";
|
|
208
|
+
}
|
|
209
|
+
return JSON.stringify(value) ?? "null";
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Orders two texts by code point (locale-independent, so every instance sorts keys the same).
|
|
213
|
+
* @param left One text.
|
|
214
|
+
* @param right The other.
|
|
215
|
+
* @returns Negative, zero or positive.
|
|
216
|
+
*/
|
|
217
|
+
function compareCodePoints(left, right) {
|
|
218
|
+
if (left < right)
|
|
219
|
+
return -1;
|
|
220
|
+
if (left > right)
|
|
221
|
+
return 1;
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Whether a record passes the filters.
|
|
226
|
+
* @param record The record.
|
|
227
|
+
* @param filters The filters.
|
|
228
|
+
* @returns True when it matches every filter given.
|
|
229
|
+
*/
|
|
230
|
+
function matchesQuery(record, filters) {
|
|
231
|
+
const actorText = (record.actor.name + " " + record.actor.id + " " + record.actor.kind).toLowerCase();
|
|
232
|
+
if (filters.actor != null && !actorText.includes(filters.actor.toLowerCase()))
|
|
233
|
+
return false;
|
|
234
|
+
if (filters.action != null && !record.action.startsWith(filters.action))
|
|
235
|
+
return false;
|
|
236
|
+
if (filters.target != null && !(record.target ?? "").toLowerCase().includes(filters.target.toLowerCase()))
|
|
237
|
+
return false;
|
|
238
|
+
if (filters.result != null && record.result.ok !== (filters.result === "ok"))
|
|
239
|
+
return false;
|
|
240
|
+
if (filters.from != null && record.at < filters.from)
|
|
241
|
+
return false;
|
|
242
|
+
if (filters.to != null && record.at > filters.to)
|
|
243
|
+
return false;
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* One CSV row of a record (CSV_COLUMNS).
|
|
248
|
+
* @param record The record.
|
|
249
|
+
* @returns The row.
|
|
250
|
+
*/
|
|
251
|
+
function csvRow(record) {
|
|
252
|
+
const actorKind = record.actor.kind;
|
|
253
|
+
const fields = [
|
|
254
|
+
String(record.sequence), record.at, actorKind, record.actor.name, record.clientAddress ?? "", record.processId,
|
|
255
|
+
record.action, record.target ?? "", record.result.ok ? "ok" : "failed", record.result.detail ?? ""
|
|
256
|
+
];
|
|
257
|
+
return fields.map(csvField).join(",");
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Quotes a CSV field when it needs it.
|
|
261
|
+
* @param field The text.
|
|
262
|
+
* @returns The CSV field.
|
|
263
|
+
*/
|
|
264
|
+
function csvField(field) {
|
|
265
|
+
return CSV_SPECIAL_CHARACTERS.test(field) ? "\"" + field.replace(/"/g, "\"\"") + "\"" : field;
|
|
266
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs `babylon-game-server admin <command> …` and sets the exit code: 0 on success, 2 on a usage or config problem,
|
|
3
|
+
* 1 otherwise.
|
|
4
|
+
* @param argumentList The arguments after `admin`.
|
|
5
|
+
* @param environment The process environment.
|
|
6
|
+
*/
|
|
7
|
+
export declare function runAdminCommand(argumentList: string[], environment: NodeJS.ProcessEnv): Promise<void>;
|