@serverkgg/bridge 0.0.21 → 0.1.2
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/package.json +3 -1
- package/src/bot/botDeploy.test.ts +363 -0
- package/src/bot/botDeploy.ts +136 -0
- package/src/bot/botEnvironment.test.ts +189 -0
- package/src/bot/botEnvironment.ts +141 -0
- package/src/bot/botIdentity.test.ts +251 -0
- package/src/bot/botIdentity.ts +140 -0
- package/src/bot/botPanel.test.ts +246 -0
- package/src/bot/botPanel.ts +129 -0
- package/src/bot/botState.test.ts +45 -0
- package/src/bot/botState.ts +54 -0
- package/src/bot/botTerminal.test.ts +79 -0
- package/src/bot/botTerminal.ts +62 -0
- package/src/bot/botVariables.test.ts +193 -0
- package/src/bot/botVariables.ts +73 -0
- package/src/bot/detect/botFramework.ts +23 -0
- package/src/bot/detect/botFrameworks.ts +198 -0
- package/src/bot/detect/botInspectWarning.ts +15 -0
- package/src/bot/detect/botPackageManager.ts +6 -0
- package/src/bot/detect/botPlatform.ts +10 -0
- package/src/bot/detect/botRuntime.ts +15 -0
- package/src/bot/detect/botToken.test.ts +71 -0
- package/src/bot/detect/botToken.ts +126 -0
- package/src/bot/detect/detect.test.ts +543 -0
- package/src/bot/detect/detect.ts +842 -0
- package/src/bot/detect/detectResult.ts +40 -0
- package/src/bot/detect/detectTable.test.ts +72 -0
- package/src/bot/detect/detectTable.ts +166 -0
- package/src/bot/detect/envExample.test.ts +29 -0
- package/src/bot/detect/envExample.ts +48 -0
- package/src/bot/detect/envKey.ts +7 -0
- package/src/bot/detect/envScan.test.ts +39 -0
- package/src/bot/detect/envScan.ts +87 -0
- package/src/bot/detect/index.ts +17 -0
- package/src/bot/detect/inferBotPlatform.test.ts +60 -0
- package/src/bot/detect/inferBotPlatform.ts +28 -0
- package/src/bot/detect/keys.test.ts +28 -0
- package/src/bot/detect/keys.ts +64 -0
- package/src/bot/detect/sensitiveKey.test.ts +91 -0
- package/src/bot/detect/sensitiveKey.ts +44 -0
- package/src/bot/detect/serverkJson.test.ts +88 -0
- package/src/bot/detect/serverkJson.ts +174 -0
- package/src/bot/index.ts +8 -0
- package/src/cli/detectTable.test.ts +66 -0
- package/src/cli/detectTable.ts +43 -0
- package/src/cli/main.ts +12 -4
- package/src/cli/renderedFile.ts +35 -0
- package/src/cli/schema.ts +7 -26
- package/src/cli/validate.test.ts +283 -3
- package/src/cli/validate.ts +105 -7
- package/src/context/bridgeContext.ts +17 -1
- package/src/core/bridgeAction.ts +2 -0
- package/src/core/bridgeActivity.ts +9 -0
- package/src/core/bridgeAvailability.ts +10 -0
- package/src/core/bridgePort.test.ts +45 -0
- package/src/core/bridgePort.ts +26 -0
- package/src/core/bridgeUserError.test.ts +10 -0
- package/src/core/bridgeUserError.ts +6 -2
- package/src/core/describeDownloadFailure.test.ts +58 -0
- package/src/core/describeDownloadFailure.ts +69 -0
- package/src/core/index.ts +3 -0
- package/src/guides/compile.ts +2 -2
- package/src/manifest/index.ts +1 -0
- package/src/manifest/kind.ts +5 -0
- package/src/manifest/manifest.test.ts +630 -3
- package/src/manifest/manifest.ts +391 -87
- package/src/manifest/runtime.ts +2 -2
- package/src/manifest/source.ts +4 -4
- package/src/modules/bridge.ts +8 -1
- package/src/modules/bridgeLifecycleSubject.ts +8 -0
- package/src/modules/bridgeModule.ts +19 -1
- package/src/modules/bridgeStopSignal.ts +13 -0
- package/src/modules/index.ts +2 -0
- package/src/panel/bridgeDetail.ts +4 -1
- package/src/protocol/bridgeActivity.ts +54 -0
- package/src/protocol/bridgeConfig.ts +2 -0
- package/src/protocol/bridgeEvents.ts +30 -0
- package/src/protocol/bridgeFrame.ts +10 -1
- package/src/protocol/index.ts +1 -0
- package/src/runtime/bridgeActions.ts +19 -2
- package/src/runtime/bridgeActivity.test.ts +318 -0
- package/src/runtime/bridgeActivity.ts +268 -0
- package/src/runtime/bridgeConfigReader.test.ts +80 -0
- package/src/runtime/bridgeConfigReader.ts +20 -0
- package/src/runtime/bridgeContext.test.ts +247 -0
- package/src/runtime/bridgeContext.ts +43 -10
- package/src/runtime/bridgeDriver.test.ts +153 -1
- package/src/runtime/bridgeDriver.ts +64 -8
- package/src/runtime/bridgeFiles.test.ts +62 -0
- package/src/runtime/bridgeFiles.ts +12 -2
- package/src/runtime/bridgeLiveConfig.ts +18 -0
- package/src/runtime/bridgeLog.ts +10 -0
- package/src/runtime/bridgeRedaction.test.ts +410 -0
- package/src/runtime/bridgeRedaction.ts +214 -0
- package/src/runtime/bridgeRefusalError.ts +13 -0
- package/src/runtime/bridgeRegistry.test.ts +1056 -10
- package/src/runtime/bridgeRegistry.ts +464 -50
- package/src/runtime/bridgeScopedContext.ts +55 -0
- package/src/runtime/bridgeServer.test.ts +534 -4
- package/src/runtime/bridgeServer.ts +145 -38
- package/src/runtime/bridgeSetupHandlers.test.ts +156 -5
- package/src/runtime/bridgeSetupHandlers.ts +59 -14
- package/src/runtime/bridgeSupervisor.test.ts +670 -2
- package/src/runtime/bridgeSupervisor.ts +219 -67
- package/src/runtime/serve.ts +87 -11
- package/src/steam/steamcmd.test.ts +32 -1
- package/src/steam/steamcmd.ts +27 -4
- package/src/utils/changedSettingKeys.test.ts +65 -0
- package/src/utils/changedSettingKeys.ts +6 -0
- package/src/utils/describeExecFailure.test.ts +34 -0
- package/src/utils/describeExecFailure.ts +9 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/sameSettingValue.test.ts +35 -0
- package/src/utils/sameSettingValue.ts +40 -0
- package/src/wire/activity.test.ts +95 -0
- package/src/wire/activity.ts +41 -0
- package/src/wire/index.ts +1 -0
- package/src/wire/panel.test.ts +67 -1
- package/src/wire/panel.ts +11 -0
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { BridgeContext, BridgeReply } from "../context";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
BridgeInstallOutcome,
|
|
4
|
+
BridgeKind,
|
|
5
|
+
BridgeOption,
|
|
6
|
+
BridgeRow,
|
|
7
|
+
BridgeStream,
|
|
8
|
+
BridgeUnavailable,
|
|
9
|
+
BridgeValues,
|
|
10
|
+
} from "../core";
|
|
3
11
|
import type { BridgeAddOutcome, BridgeDetail, BridgeDetailOutcome } from "../panel";
|
|
4
12
|
import type {
|
|
5
13
|
BridgeCatalogEntry,
|
|
@@ -11,6 +19,8 @@ import type { BridgeBackupSnapshot, BridgeDescribeResult, BridgePendingFile } fr
|
|
|
11
19
|
import type { BridgePreview } from "../protocol/bridgePreview";
|
|
12
20
|
import type { BridgeSetupStep } from "../protocol/bridgeSetup";
|
|
13
21
|
import type { BridgeTerminalCommand, BridgeTerminalRule } from "../terminal";
|
|
22
|
+
import type { BridgeLifecycleSubject } from "./bridgeLifecycleSubject";
|
|
23
|
+
import type { BridgeStopSignal } from "./bridgeStopSignal";
|
|
14
24
|
|
|
15
25
|
export type BridgeDescription = Omit<BridgeDescribeResult, "supported">;
|
|
16
26
|
|
|
@@ -25,7 +35,11 @@ export interface BridgeLifecycleModule {
|
|
|
25
35
|
ready?: RegExp;
|
|
26
36
|
failed?: RegExp;
|
|
27
37
|
stopTimeoutSeconds?: number;
|
|
38
|
+
readyTimeoutSeconds?: number;
|
|
39
|
+
stopSignal?: BridgeStopSignal;
|
|
40
|
+
subject?: BridgeLifecycleSubject;
|
|
28
41
|
command(context: BridgeContext): Promise<string[]>;
|
|
42
|
+
environment?(context: BridgeContext): Promise<Record<string, string>>;
|
|
29
43
|
stop?(context: BridgeContext): Promise<void>;
|
|
30
44
|
onReady?(context: BridgeContext): Promise<void>;
|
|
31
45
|
}
|
|
@@ -109,6 +123,7 @@ export interface BridgeSettingsModule {
|
|
|
109
123
|
protectedActions?: string[];
|
|
110
124
|
read(context: BridgeContext): Promise<BridgeValues>;
|
|
111
125
|
write?(context: BridgeContext, values: BridgeValues): Promise<void>;
|
|
126
|
+
unavailable?(context: BridgeContext): Promise<BridgeUnavailable>;
|
|
112
127
|
}
|
|
113
128
|
|
|
114
129
|
export type BridgeRowAction<Row extends BridgeRow = BridgeRow> = (
|
|
@@ -124,6 +139,7 @@ export interface BridgeActionsModule {
|
|
|
124
139
|
protectedActions?: string[];
|
|
125
140
|
requiresRunning?: boolean;
|
|
126
141
|
actions: Record<string, BridgeServerAction>;
|
|
142
|
+
unavailable?(context: BridgeContext): Promise<BridgeUnavailable>;
|
|
127
143
|
}
|
|
128
144
|
|
|
129
145
|
export interface BridgeCollectionModule<Row extends BridgeRow = BridgeRow> {
|
|
@@ -134,6 +150,7 @@ export interface BridgeCollectionModule<Row extends BridgeRow = BridgeRow> {
|
|
|
134
150
|
list(context: BridgeContext): Promise<Row[]>;
|
|
135
151
|
add?(context: BridgeContext, input: string): Promise<BridgeAddOutcome | undefined>;
|
|
136
152
|
actions?: Record<string, BridgeRowAction<Row>>;
|
|
153
|
+
unavailable?(context: BridgeContext, row: Row): Promise<BridgeUnavailable>;
|
|
137
154
|
}
|
|
138
155
|
|
|
139
156
|
export type BridgeDetailAction = (context: BridgeContext, args: BridgeValues) => Promise<BridgeDetailOutcome | null>;
|
|
@@ -158,6 +175,7 @@ export interface BridgeCatalogModule {
|
|
|
158
175
|
install(context: BridgeContext, id: string, releaseId?: string): Promise<BridgeCatalogEntry>;
|
|
159
176
|
remove(context: BridgeContext, id: string): Promise<void>;
|
|
160
177
|
toggle?(context: BridgeContext, id: string, enabled: boolean): Promise<void>;
|
|
178
|
+
unavailable?(context: BridgeContext, entry: BridgeCatalogEntry): Promise<BridgeUnavailable>;
|
|
161
179
|
}
|
|
162
180
|
|
|
163
181
|
export type BridgePanelModule =
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export enum BridgeStopSignal {
|
|
2
|
+
Term = "SIGTERM",
|
|
3
|
+
Interrupt = "SIGINT",
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const BRIDGE_STOP_SIGNALS: Record<BridgeStopSignal, NodeJS.Signals> = {
|
|
7
|
+
[BridgeStopSignal.Term]: "SIGTERM",
|
|
8
|
+
[BridgeStopSignal.Interrupt]: "SIGINT",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const isBridgeStopSignal = (value: unknown): value is BridgeStopSignal => {
|
|
12
|
+
return typeof value === "string" && Object.hasOwn(BRIDGE_STOP_SIGNALS, value);
|
|
13
|
+
};
|
package/src/modules/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { BridgeCopy, BridgeValue, LocalizedText } from "../core";
|
|
1
|
+
import type { BridgeCopy, BridgeUnavailable, BridgeValue, LocalizedText } from "../core";
|
|
2
|
+
import type { BridgeIcon } from "./bridgeIcon";
|
|
2
3
|
|
|
3
4
|
export enum BridgeDetailTone {
|
|
4
5
|
Neutral = "neutral",
|
|
@@ -38,11 +39,13 @@ export interface BridgeDetail {
|
|
|
38
39
|
subtitle: BridgeCopy | null;
|
|
39
40
|
description: BridgeCopy | null;
|
|
40
41
|
image: string | null;
|
|
42
|
+
icon?: BridgeIcon;
|
|
41
43
|
badges: BridgeDetailBadge[];
|
|
42
44
|
stats: BridgeDetailStat[];
|
|
43
45
|
links: BridgeDetailLink[];
|
|
44
46
|
stale: boolean;
|
|
45
47
|
actions: string[];
|
|
48
|
+
unavailable?: BridgeUnavailable;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
export interface BridgeDetailOutcome {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { BridgeActivity, BridgeValues } from "../core";
|
|
2
|
+
|
|
3
|
+
export const BRIDGE_ACTIVITY_KEYS = {
|
|
4
|
+
ar: "activityAr",
|
|
5
|
+
en: "activityEn",
|
|
6
|
+
subject: "subject",
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
export const BRIDGE_ACTIVITY_LIMIT = 8;
|
|
10
|
+
|
|
11
|
+
export const BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT = 256;
|
|
12
|
+
|
|
13
|
+
export const BRIDGE_ACTIVITY_SUBJECT_LENGTH_LIMIT = 64;
|
|
14
|
+
|
|
15
|
+
export const BRIDGE_ACTIVITY_DETAILS_LIMIT = 16;
|
|
16
|
+
|
|
17
|
+
export const BRIDGE_ACTIVITY_DETAIL_KEY_LENGTH_LIMIT = 64;
|
|
18
|
+
|
|
19
|
+
export const BRIDGE_ACTIVITY_EXCERPT_LENGTH_LIMIT = 80;
|
|
20
|
+
|
|
21
|
+
export const BRIDGE_ACTIVITY_NAME_LENGTH_LIMIT = 64;
|
|
22
|
+
|
|
23
|
+
export const BRIDGE_ACTIVITY_NAME_PATTERN = /^[A-Z][A-Za-z0-9]*$/;
|
|
24
|
+
|
|
25
|
+
export const BRIDGE_ACTIVITY_DETAIL_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
26
|
+
|
|
27
|
+
const ELLIPSIS = "…";
|
|
28
|
+
|
|
29
|
+
export const isBridgeActivity = (payload: BridgeValues | null): boolean => {
|
|
30
|
+
return typeof payload?.[BRIDGE_ACTIVITY_KEYS.ar] === "string" && typeof payload[BRIDGE_ACTIVITY_KEYS.en] === "string";
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const activityPayload = (entry: BridgeActivity): BridgeValues => {
|
|
34
|
+
return {
|
|
35
|
+
...entry.details,
|
|
36
|
+
[BRIDGE_ACTIVITY_KEYS.ar]: entry.text.ar,
|
|
37
|
+
[BRIDGE_ACTIVITY_KEYS.en]: entry.text.en,
|
|
38
|
+
...(entry.subject === undefined
|
|
39
|
+
? {}
|
|
40
|
+
: {
|
|
41
|
+
[BRIDGE_ACTIVITY_KEYS.subject]: entry.subject,
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const activityExcerpt = (text: string): string => {
|
|
47
|
+
const line = text.trim();
|
|
48
|
+
|
|
49
|
+
if (line.length <= BRIDGE_ACTIVITY_EXCERPT_LENGTH_LIMIT) {
|
|
50
|
+
return line;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return `${line.slice(0, BRIDGE_ACTIVITY_EXCERPT_LENGTH_LIMIT - ELLIPSIS.length).trimEnd()}${ELLIPSIS}`;
|
|
54
|
+
};
|
|
@@ -1,23 +1,53 @@
|
|
|
1
1
|
export enum BridgeEventName {
|
|
2
|
+
AccessGranted = "AccessGranted",
|
|
3
|
+
AccessRevoked = "AccessRevoked",
|
|
4
|
+
BroadcastSent = "BroadcastSent",
|
|
5
|
+
GameInstalled = "GameInstalled",
|
|
2
6
|
GameModeChanged = "GameModeChanged",
|
|
7
|
+
GameplayChanged = "GameplayChanged",
|
|
8
|
+
InstallFailed = "InstallFailed",
|
|
3
9
|
MemoryUndersized = "MemoryUndersized",
|
|
4
10
|
MissingDependency = "MissingDependency",
|
|
5
11
|
ModCrashed = "ModCrashed",
|
|
12
|
+
ModDisabled = "ModDisabled",
|
|
13
|
+
ModEnabled = "ModEnabled",
|
|
14
|
+
ModInstalled = "ModInstalled",
|
|
6
15
|
ModLoaded = "ModLoaded",
|
|
16
|
+
ModLoaderInstalled = "ModLoaderInstalled",
|
|
17
|
+
ModLoaderRemoved = "ModLoaderRemoved",
|
|
18
|
+
ModRemoved = "ModRemoved",
|
|
19
|
+
ModpackInstalled = "ModpackInstalled",
|
|
20
|
+
ModpackRemoved = "ModpackRemoved",
|
|
21
|
+
ModpackSelected = "ModpackSelected",
|
|
7
22
|
PlayerAdvanced = "PlayerAdvanced",
|
|
8
23
|
PlayerBanned = "PlayerBanned",
|
|
24
|
+
PlayerChanged = "PlayerChanged",
|
|
9
25
|
PlayerDied = "PlayerDied",
|
|
10
26
|
PlayerJoined = "PlayerJoined",
|
|
11
27
|
PlayerKicked = "PlayerKicked",
|
|
12
28
|
PlayerLeft = "PlayerLeft",
|
|
29
|
+
PlayerUnbanned = "PlayerUnbanned",
|
|
13
30
|
PortBindFailed = "PortBindFailed",
|
|
31
|
+
SecretRotated = "SecretRotated",
|
|
14
32
|
ServerCrashed = "ServerCrashed",
|
|
15
33
|
ServerStarted = "ServerStarted",
|
|
16
34
|
ServerStopping = "ServerStopping",
|
|
17
35
|
ServerUpdated = "ServerUpdated",
|
|
36
|
+
SettingsApplied = "SettingsApplied",
|
|
37
|
+
SettingsChanged = "SettingsChanged",
|
|
38
|
+
SetupAction = "SetupAction",
|
|
39
|
+
SetupCompleted = "SetupCompleted",
|
|
40
|
+
SetupRejected = "SetupRejected",
|
|
41
|
+
SignInExpired = "SignInExpired",
|
|
18
42
|
TickLagging = "TickLagging",
|
|
43
|
+
WorldActivated = "WorldActivated",
|
|
44
|
+
WorldCloned = "WorldCloned",
|
|
19
45
|
WorldCorrupt = "WorldCorrupt",
|
|
46
|
+
WorldCreated = "WorldCreated",
|
|
47
|
+
WorldDeleted = "WorldDeleted",
|
|
48
|
+
WorldImported = "WorldImported",
|
|
20
49
|
WorldSaved = "WorldSaved",
|
|
50
|
+
WorldWiped = "WorldWiped",
|
|
21
51
|
WrongJavaVersion = "WrongJavaVersion",
|
|
22
52
|
}
|
|
23
53
|
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
BridgeActivity,
|
|
3
|
+
BridgeDesired,
|
|
4
|
+
BridgeInstallHold,
|
|
5
|
+
BridgeRuntime,
|
|
6
|
+
BridgeValues,
|
|
7
|
+
LocalizedText,
|
|
8
|
+
} from "../core";
|
|
2
9
|
import type { BridgeSetupSummary } from "./bridgeSetup";
|
|
3
10
|
|
|
4
11
|
export const BRIDGE_SOCKET_NAME = "bridge.sock";
|
|
@@ -59,6 +66,7 @@ export interface BridgeSuccess {
|
|
|
59
66
|
id: number | null;
|
|
60
67
|
ok: true;
|
|
61
68
|
result: unknown;
|
|
69
|
+
activity?: BridgeActivity[];
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
export interface BridgeFailure {
|
|
@@ -68,6 +76,7 @@ export interface BridgeFailure {
|
|
|
68
76
|
secret?: string;
|
|
69
77
|
failure?: string;
|
|
70
78
|
text?: LocalizedText;
|
|
79
|
+
unchanged?: boolean;
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
export type BridgeResponse = BridgeFailure | BridgeSuccess;
|
package/src/protocol/index.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { BridgeAction, BridgeKind } from "../core";
|
|
2
2
|
import type { BridgePanelModule } from "../modules";
|
|
3
3
|
|
|
4
|
+
const advertisedAvailability = (advertised: boolean): string[] => {
|
|
5
|
+
return advertised
|
|
6
|
+
? [
|
|
7
|
+
BridgeAction.Availability,
|
|
8
|
+
]
|
|
9
|
+
: [];
|
|
10
|
+
};
|
|
11
|
+
|
|
4
12
|
export const actionsOf = (module: BridgePanelModule): string[] => {
|
|
5
13
|
switch (module.kind) {
|
|
6
14
|
case BridgeKind.Actions: {
|
|
7
|
-
return
|
|
15
|
+
return [
|
|
16
|
+
...Object.keys(module.actions),
|
|
17
|
+
...advertisedAvailability(module.unavailable !== undefined),
|
|
18
|
+
];
|
|
8
19
|
}
|
|
9
20
|
|
|
10
21
|
case BridgeKind.Options: {
|
|
@@ -27,6 +38,7 @@ export const actionsOf = (module: BridgePanelModule): string[] => {
|
|
|
27
38
|
BridgeAction.Write,
|
|
28
39
|
]
|
|
29
40
|
: []),
|
|
41
|
+
...advertisedAvailability(module.unavailable !== undefined),
|
|
30
42
|
];
|
|
31
43
|
}
|
|
32
44
|
|
|
@@ -39,13 +51,17 @@ export const actionsOf = (module: BridgePanelModule): string[] => {
|
|
|
39
51
|
]
|
|
40
52
|
: []),
|
|
41
53
|
...Object.keys(module.actions ?? {}),
|
|
54
|
+
...advertisedAvailability(module.unavailable !== undefined),
|
|
42
55
|
];
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
case BridgeKind.Detail: {
|
|
59
|
+
const actions = Object.keys(module.actions ?? {});
|
|
60
|
+
|
|
46
61
|
return [
|
|
47
62
|
BridgeAction.Read,
|
|
48
|
-
...
|
|
63
|
+
...actions,
|
|
64
|
+
...advertisedAvailability(actions.length > 0),
|
|
49
65
|
];
|
|
50
66
|
}
|
|
51
67
|
|
|
@@ -70,6 +86,7 @@ export const actionsOf = (module: BridgePanelModule): string[] => {
|
|
|
70
86
|
BridgeAction.Toggle,
|
|
71
87
|
]
|
|
72
88
|
: []),
|
|
89
|
+
...advertisedAvailability(module.unavailable !== undefined),
|
|
73
90
|
];
|
|
74
91
|
}
|
|
75
92
|
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { BridgeActivity, BridgeValues } from "../core";
|
|
3
|
+
import {
|
|
4
|
+
BRIDGE_ACTIVITY_DETAILS_LIMIT,
|
|
5
|
+
BRIDGE_ACTIVITY_KEYS,
|
|
6
|
+
BRIDGE_ACTIVITY_LIMIT,
|
|
7
|
+
BRIDGE_ACTIVITY_SUBJECT_LENGTH_LIMIT,
|
|
8
|
+
BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT,
|
|
9
|
+
BridgeEventName,
|
|
10
|
+
} from "../protocol";
|
|
11
|
+
import {
|
|
12
|
+
type ActivityNormalizeOptions,
|
|
13
|
+
createActivityReporter,
|
|
14
|
+
createActivityScope,
|
|
15
|
+
normalizeActivity,
|
|
16
|
+
} from "./bridgeActivity";
|
|
17
|
+
|
|
18
|
+
const SECRET = "tok-1234567890-secret";
|
|
19
|
+
|
|
20
|
+
const recorder = () => {
|
|
21
|
+
const warnings: {
|
|
22
|
+
message: string;
|
|
23
|
+
fields?: Record<string, unknown>;
|
|
24
|
+
}[] = [];
|
|
25
|
+
|
|
26
|
+
const emitted: {
|
|
27
|
+
event: string;
|
|
28
|
+
payload: BridgeValues;
|
|
29
|
+
}[] = [];
|
|
30
|
+
|
|
31
|
+
const options: ActivityNormalizeOptions = {
|
|
32
|
+
secretKeys: new Set([
|
|
33
|
+
"RCON_PASSWORD",
|
|
34
|
+
]),
|
|
35
|
+
redact: (text) => text.split(SECRET).join("••••"),
|
|
36
|
+
warn: (message, fields) => {
|
|
37
|
+
warnings.push({
|
|
38
|
+
message,
|
|
39
|
+
fields,
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const reporter = createActivityReporter({
|
|
45
|
+
...options,
|
|
46
|
+
onEmit(event, payload) {
|
|
47
|
+
emitted.push({
|
|
48
|
+
event,
|
|
49
|
+
payload,
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
options,
|
|
56
|
+
reporter,
|
|
57
|
+
warnings,
|
|
58
|
+
emitted,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const granted = (subject = "Steve"): BridgeActivity => ({
|
|
63
|
+
name: BridgeEventName.AccessGranted,
|
|
64
|
+
text: {
|
|
65
|
+
ar: `أضفت ${subject} للقائمة البيضاء`,
|
|
66
|
+
en: `Added ${subject} to the whitelist`,
|
|
67
|
+
},
|
|
68
|
+
subject,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("normalizeActivity", () => {
|
|
72
|
+
test("keeps a well-formed entry as it is", () => {
|
|
73
|
+
const { options, warnings } = recorder();
|
|
74
|
+
|
|
75
|
+
expect(normalizeActivity(granted(), options)).toEqual(granted());
|
|
76
|
+
expect(warnings).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("drops a name the protocol does not know, with a warning", () => {
|
|
80
|
+
const { options, warnings } = recorder();
|
|
81
|
+
|
|
82
|
+
expect(
|
|
83
|
+
normalizeActivity(
|
|
84
|
+
{
|
|
85
|
+
...granted(),
|
|
86
|
+
name: "WhitelistAdded" as BridgeEventName,
|
|
87
|
+
},
|
|
88
|
+
options,
|
|
89
|
+
),
|
|
90
|
+
).toBeNull();
|
|
91
|
+
expect(warnings).toEqual([
|
|
92
|
+
{
|
|
93
|
+
message: "activity entry normalized",
|
|
94
|
+
fields: {
|
|
95
|
+
name: "WhitelistAdded",
|
|
96
|
+
reason: "unknown-name",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("drops an entry whose text is empty in one language", () => {
|
|
103
|
+
const { options } = recorder();
|
|
104
|
+
|
|
105
|
+
expect(
|
|
106
|
+
normalizeActivity(
|
|
107
|
+
{
|
|
108
|
+
...granted(),
|
|
109
|
+
text: {
|
|
110
|
+
ar: " ",
|
|
111
|
+
en: "Added Steve",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
options,
|
|
115
|
+
),
|
|
116
|
+
).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("cuts the text and the subject to their limits", () => {
|
|
120
|
+
const { options, warnings } = recorder();
|
|
121
|
+
|
|
122
|
+
const normalized = normalizeActivity(
|
|
123
|
+
{
|
|
124
|
+
...granted(),
|
|
125
|
+
text: {
|
|
126
|
+
ar: "ا".repeat(BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT + 10),
|
|
127
|
+
en: "a".repeat(BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT + 10),
|
|
128
|
+
},
|
|
129
|
+
subject: "s".repeat(BRIDGE_ACTIVITY_SUBJECT_LENGTH_LIMIT + 10),
|
|
130
|
+
},
|
|
131
|
+
options,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
expect(normalized?.text.ar).toHaveLength(BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT);
|
|
135
|
+
expect(normalized?.text.en).toHaveLength(BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT);
|
|
136
|
+
expect(normalized?.subject).toHaveLength(BRIDGE_ACTIVITY_SUBJECT_LENGTH_LIMIT);
|
|
137
|
+
expect(warnings.map((warning) => warning.fields?.reason)).toEqual([
|
|
138
|
+
"long-text",
|
|
139
|
+
"long-subject",
|
|
140
|
+
]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("keeps at most sixteen details", () => {
|
|
144
|
+
const { options } = recorder();
|
|
145
|
+
|
|
146
|
+
const details: BridgeValues = {};
|
|
147
|
+
|
|
148
|
+
for (let index = 0; index < BRIDGE_ACTIVITY_DETAILS_LIMIT + 4; index++) {
|
|
149
|
+
details[`key${index}`] = index;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const normalized = normalizeActivity(
|
|
153
|
+
{
|
|
154
|
+
...granted(),
|
|
155
|
+
details,
|
|
156
|
+
},
|
|
157
|
+
options,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(Object.keys(normalized?.details ?? {})).toHaveLength(BRIDGE_ACTIVITY_DETAILS_LIMIT);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("removes secret fields, reserved keys and values that are not scalars", () => {
|
|
164
|
+
const { options } = recorder();
|
|
165
|
+
|
|
166
|
+
const normalized = normalizeActivity(
|
|
167
|
+
{
|
|
168
|
+
...granted(),
|
|
169
|
+
details: {
|
|
170
|
+
RCON_PASSWORD: "hunter22hunter22",
|
|
171
|
+
[BRIDGE_ACTIVITY_KEYS.ar]: "مزوّر",
|
|
172
|
+
"bad key": "x",
|
|
173
|
+
nested: {
|
|
174
|
+
deep: true,
|
|
175
|
+
} as unknown as string,
|
|
176
|
+
count: Number.NaN,
|
|
177
|
+
from: "1.20.4",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
options,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
expect(normalized?.details).toEqual({
|
|
184
|
+
from: "1.20.4",
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("blanks a secret value in the text, the subject and the details", () => {
|
|
189
|
+
const { options } = recorder();
|
|
190
|
+
|
|
191
|
+
const normalized = normalizeActivity(
|
|
192
|
+
{
|
|
193
|
+
name: BridgeEventName.BroadcastSent,
|
|
194
|
+
text: {
|
|
195
|
+
ar: `أرسلت ${SECRET}`,
|
|
196
|
+
en: `Sent ${SECRET}`,
|
|
197
|
+
},
|
|
198
|
+
subject: SECRET,
|
|
199
|
+
details: {
|
|
200
|
+
message: `hello ${SECRET}`,
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
options,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
expect(JSON.stringify(normalized)).not.toContain(SECRET);
|
|
207
|
+
expect(normalized?.details).toEqual({
|
|
208
|
+
message: "hello ••••",
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("createActivityReporter", () => {
|
|
214
|
+
test("emits the entry as an event carrying the text under the reserved keys", () => {
|
|
215
|
+
const { reporter, emitted } = recorder();
|
|
216
|
+
|
|
217
|
+
reporter.report({
|
|
218
|
+
...granted(),
|
|
219
|
+
details: {
|
|
220
|
+
source: "panel",
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(emitted).toEqual([
|
|
225
|
+
{
|
|
226
|
+
event: BridgeEventName.AccessGranted,
|
|
227
|
+
payload: {
|
|
228
|
+
source: "panel",
|
|
229
|
+
[BRIDGE_ACTIVITY_KEYS.ar]: "أضفت Steve للقائمة البيضاء",
|
|
230
|
+
[BRIDGE_ACTIVITY_KEYS.en]: "Added Steve to the whitelist",
|
|
231
|
+
[BRIDGE_ACTIVITY_KEYS.subject]: "Steve",
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
]);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("emits nothing for an entry normalisation drops", () => {
|
|
238
|
+
const { reporter, emitted } = recorder();
|
|
239
|
+
|
|
240
|
+
reporter.report({
|
|
241
|
+
...granted(),
|
|
242
|
+
name: "Nope" as BridgeEventName,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(emitted).toEqual([]);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe("createActivityScope", () => {
|
|
250
|
+
test("collects entries for the answer instead of emitting them", () => {
|
|
251
|
+
const { options, reporter, emitted } = recorder();
|
|
252
|
+
const scope = createActivityScope(reporter, options);
|
|
253
|
+
|
|
254
|
+
scope.reporter.report(granted("Steve"));
|
|
255
|
+
scope.reporter.report(granted("Alex"));
|
|
256
|
+
|
|
257
|
+
expect(scope.reported).toBe(2);
|
|
258
|
+
expect(scope.close()).toEqual([
|
|
259
|
+
granted("Steve"),
|
|
260
|
+
granted("Alex"),
|
|
261
|
+
]);
|
|
262
|
+
expect(emitted).toEqual([]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("drains what it collected to the ring when the handler throws", () => {
|
|
266
|
+
const { options, reporter, emitted } = recorder();
|
|
267
|
+
const scope = createActivityScope(reporter, options);
|
|
268
|
+
|
|
269
|
+
scope.reporter.report(granted());
|
|
270
|
+
scope.drain();
|
|
271
|
+
|
|
272
|
+
expect(emitted.map((entry) => entry.event)).toEqual([
|
|
273
|
+
BridgeEventName.AccessGranted,
|
|
274
|
+
]);
|
|
275
|
+
expect(scope.close()).toEqual([]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("sends a report made after the answer to the ring", () => {
|
|
279
|
+
const { options, reporter, emitted } = recorder();
|
|
280
|
+
const scope = createActivityScope(reporter, options);
|
|
281
|
+
|
|
282
|
+
expect(scope.close()).toEqual([]);
|
|
283
|
+
|
|
284
|
+
scope.reporter.report(granted());
|
|
285
|
+
|
|
286
|
+
expect(emitted.map((entry) => entry.payload[BRIDGE_ACTIVITY_KEYS.subject])).toEqual([
|
|
287
|
+
"Steve",
|
|
288
|
+
]);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("keeps at most eight entries on one answer", () => {
|
|
292
|
+
const { options, reporter, warnings } = recorder();
|
|
293
|
+
const scope = createActivityScope(reporter, options);
|
|
294
|
+
|
|
295
|
+
for (let index = 0; index < BRIDGE_ACTIVITY_LIMIT + 2; index++) {
|
|
296
|
+
scope.reporter.report(granted(`player${index}`));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
expect(scope.close()).toHaveLength(BRIDGE_ACTIVITY_LIMIT);
|
|
300
|
+
expect(warnings.filter((warning) => warning.fields?.reason === "extra-entries")).toHaveLength(2);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("never throws into the caller, whatever the entry holds", () => {
|
|
304
|
+
const { options, reporter, warnings } = recorder();
|
|
305
|
+
const scope = createActivityScope(reporter, options);
|
|
306
|
+
|
|
307
|
+
const hostile = {
|
|
308
|
+
...granted(),
|
|
309
|
+
get details(): BridgeValues {
|
|
310
|
+
throw new Error("a getter that throws");
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
expect(() => scope.reporter.report(hostile)).not.toThrow();
|
|
315
|
+
expect(() => reporter.report(hostile)).not.toThrow();
|
|
316
|
+
expect(warnings.filter((warning) => warning.message === "activity report failed")).toHaveLength(2);
|
|
317
|
+
});
|
|
318
|
+
});
|