@serverkgg/bridge 0.0.21 → 0.1.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/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 +216 -0
- package/src/bot/botPanel.ts +121 -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 +210 -2
- package/src/cli/validate.ts +71 -7
- package/src/context/bridgeContext.ts +11 -0
- package/src/core/bridgePort.test.ts +45 -0
- package/src/core/bridgePort.ts +26 -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/bridgeLifecycleSubject.ts +8 -0
- package/src/modules/bridgeModule.ts +6 -0
- package/src/modules/bridgeStopSignal.ts +13 -0
- package/src/modules/index.ts +2 -0
- package/src/panel/bridgeDetail.ts +2 -0
- package/src/protocol/bridgeConfig.ts +2 -0
- package/src/runtime/bridgeConfigReader.test.ts +80 -0
- package/src/runtime/bridgeConfigReader.ts +20 -0
- package/src/runtime/bridgeContext.test.ts +241 -0
- package/src/runtime/bridgeContext.ts +35 -9
- package/src/runtime/bridgeDriver.test.ts +132 -1
- package/src/runtime/bridgeDriver.ts +54 -1
- 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/bridgeRedaction.test.ts +410 -0
- package/src/runtime/bridgeRedaction.ts +214 -0
- package/src/runtime/bridgeRegistry.test.ts +3 -0
- package/src/runtime/bridgeRegistry.ts +15 -0
- package/src/runtime/bridgeServer.test.ts +130 -0
- package/src/runtime/bridgeServer.ts +24 -7
- package/src/runtime/bridgeSetupHandlers.test.ts +88 -0
- package/src/runtime/bridgeSetupHandlers.ts +16 -6
- package/src/runtime/bridgeSupervisor.test.ts +670 -2
- package/src/runtime/bridgeSupervisor.ts +219 -67
- package/src/runtime/serve.ts +47 -13
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BridgeCopy, BridgeValue, LocalizedText } from "../core";
|
|
2
|
+
import type { BridgeIcon } from "./bridgeIcon";
|
|
2
3
|
|
|
3
4
|
export enum BridgeDetailTone {
|
|
4
5
|
Neutral = "neutral",
|
|
@@ -38,6 +39,7 @@ 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[];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { BridgeConfigError, parseConfig } from "./bridgeConfigReader";
|
|
3
|
+
|
|
4
|
+
const written = (extra: Record<string, unknown> = {}) => {
|
|
5
|
+
return JSON.stringify({
|
|
6
|
+
code: "abcdef",
|
|
7
|
+
token: "token",
|
|
8
|
+
cwd: "/home/game",
|
|
9
|
+
run: "/run/serverk",
|
|
10
|
+
ports: {
|
|
11
|
+
game: 19_132,
|
|
12
|
+
media: 20_000,
|
|
13
|
+
},
|
|
14
|
+
memoryMb: 2048,
|
|
15
|
+
cpuPercent: 100,
|
|
16
|
+
gameUid: 1000,
|
|
17
|
+
gameGid: 1000,
|
|
18
|
+
variables: {},
|
|
19
|
+
secrets: {},
|
|
20
|
+
...extra,
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe("parseConfig", () => {
|
|
25
|
+
test("reads a file an older agent wrote, with no public address and no port counts", () => {
|
|
26
|
+
const config = parseConfig(written());
|
|
27
|
+
|
|
28
|
+
expect(config.publicAddress).toBeUndefined();
|
|
29
|
+
expect(config.portCounts).toBeUndefined();
|
|
30
|
+
expect(config.ports).toEqual({
|
|
31
|
+
game: 19_132,
|
|
32
|
+
media: 20_000,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("reads the public address and the port counts when the agent wrote them", () => {
|
|
37
|
+
const config = parseConfig(
|
|
38
|
+
written({
|
|
39
|
+
publicAddress: "203.0.113.7",
|
|
40
|
+
portCounts: {
|
|
41
|
+
game: 1,
|
|
42
|
+
media: 32,
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(config.publicAddress).toBe("203.0.113.7");
|
|
48
|
+
expect(config.portCounts).toEqual({
|
|
49
|
+
game: 1,
|
|
50
|
+
media: 32,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("ignores a malformed public address and drops non-numeric counts", () => {
|
|
55
|
+
const config = parseConfig(
|
|
56
|
+
written({
|
|
57
|
+
publicAddress: 7,
|
|
58
|
+
portCounts: {
|
|
59
|
+
media: "32",
|
|
60
|
+
game: 1,
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(config.publicAddress).toBeUndefined();
|
|
66
|
+
expect(config.portCounts).toEqual({
|
|
67
|
+
game: 1,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("still refuses a game that would run as root", () => {
|
|
72
|
+
expect(() =>
|
|
73
|
+
parseConfig(
|
|
74
|
+
written({
|
|
75
|
+
gameUid: 0,
|
|
76
|
+
}),
|
|
77
|
+
),
|
|
78
|
+
).toThrow(BridgeConfigError);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -70,6 +70,24 @@ const stringRecord = (source: Record<string, unknown>, key: string) => {
|
|
|
70
70
|
return out;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
const optionalString = (source: Record<string, unknown>, key: string) => {
|
|
74
|
+
const value = source[key];
|
|
75
|
+
|
|
76
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return value;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const optionalNumberRecord = (source: Record<string, unknown>, key: string) => {
|
|
84
|
+
if (source[key] === undefined) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return numberRecord(source, key);
|
|
89
|
+
};
|
|
90
|
+
|
|
73
91
|
export const configPath = () => {
|
|
74
92
|
return `${BRIDGE_RUN_MOUNT}/${BRIDGE_CONFIG_NAME}`;
|
|
75
93
|
};
|
|
@@ -97,6 +115,8 @@ export const parseConfig = (text: string): BridgeConfig => {
|
|
|
97
115
|
cwd: requireString(source, "cwd"),
|
|
98
116
|
run: requireString(source, "run"),
|
|
99
117
|
ports: numberRecord(source, "ports"),
|
|
118
|
+
portCounts: optionalNumberRecord(source, "portCounts"),
|
|
119
|
+
publicAddress: optionalString(source, "publicAddress"),
|
|
100
120
|
memoryMb: requireNumber(source, "memoryMb"),
|
|
101
121
|
cpuPercent: requireNumber(source, "cpuPercent"),
|
|
102
122
|
gameUid: requireNumber(source, "gameUid"),
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { BridgeConfig } from "../protocol";
|
|
6
|
+
import { createContext } from "./bridgeContext";
|
|
7
|
+
import { createBridgeLiveConfig } from "./bridgeLiveConfig";
|
|
8
|
+
import { log } from "./bridgeLog";
|
|
9
|
+
import { createSupervisor } from "./bridgeSupervisor";
|
|
10
|
+
|
|
11
|
+
describe("the context's variable maps", () => {
|
|
12
|
+
let directory = "";
|
|
13
|
+
let path = "";
|
|
14
|
+
|
|
15
|
+
const configWith = (variables: Record<string, string>, secrets: Record<string, string>): BridgeConfig => ({
|
|
16
|
+
code: "abcdef",
|
|
17
|
+
token: "token",
|
|
18
|
+
cwd: directory,
|
|
19
|
+
run: join(directory, "run"),
|
|
20
|
+
ports: {},
|
|
21
|
+
memoryMb: 512,
|
|
22
|
+
cpuPercent: 100,
|
|
23
|
+
gameUid: 1000,
|
|
24
|
+
gameGid: 1000,
|
|
25
|
+
variables,
|
|
26
|
+
secrets,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
beforeEach(async () => {
|
|
30
|
+
directory = await mkdtemp(join(tmpdir(), "bridge-context-"));
|
|
31
|
+
path = join(directory, "config.json");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(async () => {
|
|
35
|
+
await rm(directory, {
|
|
36
|
+
force: true,
|
|
37
|
+
recursive: true,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("hand a driver every variable and secret the agent wrote, and follow a rewrite", async () => {
|
|
42
|
+
const boot = configWith(
|
|
43
|
+
{
|
|
44
|
+
MY_VAR: "1",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
DISCORD_TOKEN: "sealed",
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
await writeFile(path, JSON.stringify(boot));
|
|
52
|
+
|
|
53
|
+
const live = createBridgeLiveConfig(path, boot, 60_000);
|
|
54
|
+
const supervisor = createSupervisor({
|
|
55
|
+
cwd: directory,
|
|
56
|
+
uid: 1000,
|
|
57
|
+
gid: 1000,
|
|
58
|
+
stopTimeoutSeconds: 1,
|
|
59
|
+
onLog: log,
|
|
60
|
+
resolveCommand: async () => [
|
|
61
|
+
"run",
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
const context = createContext(
|
|
65
|
+
boot,
|
|
66
|
+
supervisor,
|
|
67
|
+
{
|
|
68
|
+
onLog: log,
|
|
69
|
+
onEmit() {
|
|
70
|
+
return;
|
|
71
|
+
},
|
|
72
|
+
onSetup() {
|
|
73
|
+
return;
|
|
74
|
+
},
|
|
75
|
+
async onRestart() {
|
|
76
|
+
return;
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
live,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(context.variables()).toEqual({
|
|
83
|
+
MY_VAR: "1",
|
|
84
|
+
});
|
|
85
|
+
expect(context.secrets()).toEqual({
|
|
86
|
+
DISCORD_TOKEN: "sealed",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await writeFile(
|
|
90
|
+
path,
|
|
91
|
+
JSON.stringify(
|
|
92
|
+
configWith(
|
|
93
|
+
{
|
|
94
|
+
MY_VAR: "2",
|
|
95
|
+
LOG_LEVEL: "debug",
|
|
96
|
+
},
|
|
97
|
+
{},
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
live.reload();
|
|
103
|
+
|
|
104
|
+
expect(context.variables()).toEqual({
|
|
105
|
+
MY_VAR: "2",
|
|
106
|
+
LOG_LEVEL: "debug",
|
|
107
|
+
});
|
|
108
|
+
expect(context.secrets()).toEqual({});
|
|
109
|
+
expect(context.variable("LOG_LEVEL")).toBe("debug");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("the context's ports and address", () => {
|
|
114
|
+
let directory = "";
|
|
115
|
+
let path = "";
|
|
116
|
+
|
|
117
|
+
const configWith = (extra: Partial<BridgeConfig>): BridgeConfig => ({
|
|
118
|
+
code: "abcdef",
|
|
119
|
+
token: "token",
|
|
120
|
+
cwd: directory,
|
|
121
|
+
run: join(directory, "run"),
|
|
122
|
+
ports: {
|
|
123
|
+
game: 19_140,
|
|
124
|
+
media: 20_064,
|
|
125
|
+
},
|
|
126
|
+
memoryMb: 512,
|
|
127
|
+
cpuPercent: 100,
|
|
128
|
+
gameUid: 1000,
|
|
129
|
+
gameGid: 1000,
|
|
130
|
+
variables: {},
|
|
131
|
+
secrets: {},
|
|
132
|
+
...extra,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const contextOf = (boot: BridgeConfig) => {
|
|
136
|
+
const live = createBridgeLiveConfig(path, boot, 60_000);
|
|
137
|
+
const supervisor = createSupervisor({
|
|
138
|
+
cwd: directory,
|
|
139
|
+
uid: 1000,
|
|
140
|
+
gid: 1000,
|
|
141
|
+
stopTimeoutSeconds: 1,
|
|
142
|
+
onLog: log,
|
|
143
|
+
resolveCommand: async () => [
|
|
144
|
+
"run",
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
live,
|
|
150
|
+
context: createContext(
|
|
151
|
+
boot,
|
|
152
|
+
supervisor,
|
|
153
|
+
{
|
|
154
|
+
onLog: log,
|
|
155
|
+
onEmit() {
|
|
156
|
+
return;
|
|
157
|
+
},
|
|
158
|
+
onSetup() {
|
|
159
|
+
return;
|
|
160
|
+
},
|
|
161
|
+
async onRestart() {
|
|
162
|
+
return;
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
live,
|
|
166
|
+
),
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
beforeEach(async () => {
|
|
171
|
+
directory = await mkdtemp(join(tmpdir(), "bridge-context-ports-"));
|
|
172
|
+
path = join(directory, "config.json");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
afterEach(async () => {
|
|
176
|
+
await rm(directory, {
|
|
177
|
+
force: true,
|
|
178
|
+
recursive: true,
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("hand a driver the block the agent published and its public address", async () => {
|
|
183
|
+
const boot = configWith({
|
|
184
|
+
portCounts: {
|
|
185
|
+
game: 1,
|
|
186
|
+
media: 32,
|
|
187
|
+
},
|
|
188
|
+
publicAddress: "203.0.113.7",
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await writeFile(path, JSON.stringify(boot));
|
|
192
|
+
|
|
193
|
+
const { context } = contextOf(boot);
|
|
194
|
+
|
|
195
|
+
expect(context.portBlock("media")).toEqual({
|
|
196
|
+
first: 20_064,
|
|
197
|
+
count: 32,
|
|
198
|
+
});
|
|
199
|
+
expect(context.portBlock("game")).toEqual({
|
|
200
|
+
first: 19_140,
|
|
201
|
+
count: 1,
|
|
202
|
+
});
|
|
203
|
+
expect(context.server.publicAddress).toBe("203.0.113.7");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("treat every port as a single one and the address as unknown for a file an older agent wrote", async () => {
|
|
207
|
+
const boot = configWith({});
|
|
208
|
+
|
|
209
|
+
await writeFile(path, JSON.stringify(boot));
|
|
210
|
+
|
|
211
|
+
const { context } = contextOf(boot);
|
|
212
|
+
|
|
213
|
+
expect(context.portBlock("media")).toEqual({
|
|
214
|
+
first: 20_064,
|
|
215
|
+
count: 1,
|
|
216
|
+
});
|
|
217
|
+
expect(context.server.publicAddress).toBeNull();
|
|
218
|
+
expect(() => context.portBlock("query")).toThrow('this game declares no port named "query"');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("follow a rewrite of the public address", async () => {
|
|
222
|
+
const boot = configWith({});
|
|
223
|
+
|
|
224
|
+
await writeFile(path, JSON.stringify(boot));
|
|
225
|
+
|
|
226
|
+
const { context, live } = contextOf(boot);
|
|
227
|
+
|
|
228
|
+
await writeFile(
|
|
229
|
+
path,
|
|
230
|
+
JSON.stringify(
|
|
231
|
+
configWith({
|
|
232
|
+
publicAddress: "198.51.100.4",
|
|
233
|
+
}),
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
live.reload();
|
|
238
|
+
|
|
239
|
+
expect(context.server.publicAddress).toBe("198.51.100.4");
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -7,7 +7,7 @@ import { configPath } from "./bridgeConfigReader";
|
|
|
7
7
|
import { createBridgeEgress } from "./bridgeEgress";
|
|
8
8
|
import { createBridgeExec } from "./bridgeExec";
|
|
9
9
|
import { createBridgeFiles } from "./bridgeFiles";
|
|
10
|
-
import { createBridgeLiveConfig } from "./bridgeLiveConfig";
|
|
10
|
+
import { type BridgeLiveConfig, createBridgeLiveConfig } from "./bridgeLiveConfig";
|
|
11
11
|
import type { BridgeLeveledLogger } from "./bridgeLog";
|
|
12
12
|
import { createBridgeNet } from "./bridgeNet";
|
|
13
13
|
import { createBridgeProbes } from "./bridgeProbe";
|
|
@@ -18,17 +18,17 @@ export interface ContextHooks {
|
|
|
18
18
|
onLog: BridgeLeveledLogger;
|
|
19
19
|
onEmit: (event: string, payload?: BridgeValues) => void;
|
|
20
20
|
onSetup: (runtime: BridgeSetupRuntime | null) => void;
|
|
21
|
+
onRestart: () => Promise<void>;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export const createContext = (
|
|
24
25
|
config: BridgeConfig,
|
|
25
26
|
supervisor: BridgeSupervisor,
|
|
26
27
|
hooks: ContextHooks,
|
|
28
|
+
live: BridgeLiveConfig = createBridgeLiveConfig(configPath(), config),
|
|
27
29
|
): BridgeContext => {
|
|
28
30
|
const egress = createBridgeEgress(config);
|
|
29
31
|
|
|
30
|
-
const live = createBridgeLiveConfig(configPath(), config);
|
|
31
|
-
|
|
32
32
|
const files = createBridgeFiles({
|
|
33
33
|
root: config.cwd,
|
|
34
34
|
uid: config.gameUid,
|
|
@@ -37,6 +37,16 @@ export const createContext = (
|
|
|
37
37
|
onLog: hooks.onLog,
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
const portOf = (key: string) => {
|
|
41
|
+
const value = config.ports[key];
|
|
42
|
+
|
|
43
|
+
if (value === undefined) {
|
|
44
|
+
throw new Error(`this game declares no port named "${key}"`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return value;
|
|
48
|
+
};
|
|
49
|
+
|
|
40
50
|
return {
|
|
41
51
|
server: {
|
|
42
52
|
code: config.code,
|
|
@@ -49,24 +59,36 @@ export const createContext = (
|
|
|
49
59
|
get running() {
|
|
50
60
|
return supervisor.running;
|
|
51
61
|
},
|
|
62
|
+
get publicAddress() {
|
|
63
|
+
return live.publicAddress();
|
|
64
|
+
},
|
|
52
65
|
},
|
|
53
66
|
|
|
54
67
|
variable(key) {
|
|
55
68
|
return live.variable(key);
|
|
56
69
|
},
|
|
57
70
|
|
|
71
|
+
variables() {
|
|
72
|
+
return live.variables();
|
|
73
|
+
},
|
|
74
|
+
|
|
58
75
|
secret(key) {
|
|
59
76
|
return live.secret(key);
|
|
60
77
|
},
|
|
61
78
|
|
|
62
|
-
|
|
63
|
-
|
|
79
|
+
secrets() {
|
|
80
|
+
return live.secrets();
|
|
81
|
+
},
|
|
64
82
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
83
|
+
port(key) {
|
|
84
|
+
return portOf(key);
|
|
85
|
+
},
|
|
68
86
|
|
|
69
|
-
|
|
87
|
+
portBlock(key) {
|
|
88
|
+
return {
|
|
89
|
+
first: portOf(key),
|
|
90
|
+
count: config.portCounts?.[key] ?? 1,
|
|
91
|
+
};
|
|
70
92
|
},
|
|
71
93
|
|
|
72
94
|
files,
|
|
@@ -123,6 +145,10 @@ export const createContext = (
|
|
|
123
145
|
hooks.onEmit(event, payload);
|
|
124
146
|
},
|
|
125
147
|
|
|
148
|
+
async restart() {
|
|
149
|
+
await hooks.onRestart();
|
|
150
|
+
},
|
|
151
|
+
|
|
126
152
|
log: Object.assign(
|
|
127
153
|
(message: string, fields?: BridgeValues) => {
|
|
128
154
|
hooks.onLog(message, {
|
|
@@ -2,7 +2,13 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { BridgeKind, BridgeSetupStepKind, PortJoin, PortProtocol } from "../core";
|
|
3
3
|
import { GuideOpenTab } from "../guides";
|
|
4
4
|
import type { ManifestPort, ManifestPortActivation } from "../manifest";
|
|
5
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
type BridgeDriver,
|
|
7
|
+
BridgeLifecycleSubject,
|
|
8
|
+
type BridgePendingModule,
|
|
9
|
+
type BridgeSetupModule,
|
|
10
|
+
BridgeStopSignal,
|
|
11
|
+
} from "../modules";
|
|
6
12
|
import {
|
|
7
13
|
BridgeConfirm,
|
|
8
14
|
BridgeControl,
|
|
@@ -235,6 +241,7 @@ const port = (activeWhen: ManifestPortActivation[]): ManifestPort => ({
|
|
|
235
241
|
join: PortJoin.Combined,
|
|
236
242
|
activeWhen,
|
|
237
243
|
mirror: false,
|
|
244
|
+
count: 1,
|
|
238
245
|
});
|
|
239
246
|
|
|
240
247
|
const driverWithTabs = (tabs: number, sections: number): BridgeDriver => ({
|
|
@@ -1781,3 +1788,127 @@ describe("section placement", () => {
|
|
|
1781
1788
|
);
|
|
1782
1789
|
});
|
|
1783
1790
|
});
|
|
1791
|
+
|
|
1792
|
+
describe("the lifecycle module", () => {
|
|
1793
|
+
const lifecycleDriver = (lifecycle: Record<string, unknown>): BridgeDriver => ({
|
|
1794
|
+
install: {
|
|
1795
|
+
kind: BridgeKind.Install,
|
|
1796
|
+
run: async () => {
|
|
1797
|
+
return;
|
|
1798
|
+
},
|
|
1799
|
+
},
|
|
1800
|
+
lifecycle: {
|
|
1801
|
+
kind: BridgeKind.Lifecycle,
|
|
1802
|
+
command: async () => [
|
|
1803
|
+
"run",
|
|
1804
|
+
],
|
|
1805
|
+
...lifecycle,
|
|
1806
|
+
},
|
|
1807
|
+
});
|
|
1808
|
+
|
|
1809
|
+
test("accepts an environment resolver", () => {
|
|
1810
|
+
expect(() =>
|
|
1811
|
+
validateDriver(
|
|
1812
|
+
lifecycleDriver({
|
|
1813
|
+
environment: async () => ({
|
|
1814
|
+
BOT_TOKEN: "token",
|
|
1815
|
+
}),
|
|
1816
|
+
}),
|
|
1817
|
+
),
|
|
1818
|
+
).not.toThrow();
|
|
1819
|
+
});
|
|
1820
|
+
|
|
1821
|
+
test("accepts a lifecycle that declares no environment", () => {
|
|
1822
|
+
expect(() => validateDriver(lifecycleDriver({}))).not.toThrow();
|
|
1823
|
+
});
|
|
1824
|
+
|
|
1825
|
+
test("accepts each stop signal the bridge sends", () => {
|
|
1826
|
+
for (const stopSignal of Object.values(BridgeStopSignal)) {
|
|
1827
|
+
expect(() =>
|
|
1828
|
+
validateDriver(
|
|
1829
|
+
lifecycleDriver({
|
|
1830
|
+
stopSignal,
|
|
1831
|
+
}),
|
|
1832
|
+
),
|
|
1833
|
+
).not.toThrow();
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1836
|
+
|
|
1837
|
+
test("refuses a stop signal the bridge does not send", () => {
|
|
1838
|
+
expect(() =>
|
|
1839
|
+
validateDriver(
|
|
1840
|
+
lifecycleDriver({
|
|
1841
|
+
stopSignal: "SIGHUP",
|
|
1842
|
+
}),
|
|
1843
|
+
),
|
|
1844
|
+
).toThrow('the lifecycle module declares stop signal "SIGHUP" — the bridge sends only SIGTERM or SIGINT');
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1847
|
+
test("accepts a ready timeout from one second to an hour", () => {
|
|
1848
|
+
for (const readyTimeoutSeconds of [
|
|
1849
|
+
1,
|
|
1850
|
+
900,
|
|
1851
|
+
3600,
|
|
1852
|
+
]) {
|
|
1853
|
+
expect(() =>
|
|
1854
|
+
validateDriver(
|
|
1855
|
+
lifecycleDriver({
|
|
1856
|
+
readyTimeoutSeconds,
|
|
1857
|
+
}),
|
|
1858
|
+
),
|
|
1859
|
+
).not.toThrow();
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
test("refuses a ready timeout outside one second to an hour", () => {
|
|
1864
|
+
for (const readyTimeoutSeconds of [
|
|
1865
|
+
0,
|
|
1866
|
+
3601,
|
|
1867
|
+
1.5,
|
|
1868
|
+
]) {
|
|
1869
|
+
expect(() =>
|
|
1870
|
+
validateDriver(
|
|
1871
|
+
lifecycleDriver({
|
|
1872
|
+
readyTimeoutSeconds,
|
|
1873
|
+
}),
|
|
1874
|
+
),
|
|
1875
|
+
).toThrow(
|
|
1876
|
+
`the lifecycle module declares a ready timeout of ${readyTimeoutSeconds} seconds — it must be a whole number from 1 to 3600`,
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
|
|
1881
|
+
test("accepts each lifecycle subject the bridge names", () => {
|
|
1882
|
+
for (const subject of Object.values(BridgeLifecycleSubject)) {
|
|
1883
|
+
expect(() =>
|
|
1884
|
+
validateDriver(
|
|
1885
|
+
lifecycleDriver({
|
|
1886
|
+
subject,
|
|
1887
|
+
}),
|
|
1888
|
+
),
|
|
1889
|
+
).not.toThrow();
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1893
|
+
test("refuses a lifecycle subject the bridge does not name", () => {
|
|
1894
|
+
expect(() =>
|
|
1895
|
+
validateDriver(
|
|
1896
|
+
lifecycleDriver({
|
|
1897
|
+
subject: "server",
|
|
1898
|
+
}),
|
|
1899
|
+
),
|
|
1900
|
+
).toThrow('the lifecycle module declares subject "server" — the bridge names only game or bot');
|
|
1901
|
+
});
|
|
1902
|
+
|
|
1903
|
+
test("refuses an environment that is not a function", () => {
|
|
1904
|
+
expect(() =>
|
|
1905
|
+
validateDriver(
|
|
1906
|
+
lifecycleDriver({
|
|
1907
|
+
environment: {
|
|
1908
|
+
BOT_TOKEN: "token",
|
|
1909
|
+
},
|
|
1910
|
+
}),
|
|
1911
|
+
),
|
|
1912
|
+
).toThrow("the lifecycle module declares an environment that is not a function");
|
|
1913
|
+
});
|
|
1914
|
+
});
|
|
@@ -7,7 +7,14 @@ import {
|
|
|
7
7
|
} from "../core";
|
|
8
8
|
import { GuideOpenTab } from "../guides";
|
|
9
9
|
import type { ManifestPort } from "../manifest";
|
|
10
|
-
import
|
|
10
|
+
import {
|
|
11
|
+
type BridgeDriver,
|
|
12
|
+
BridgeLifecycleSubject,
|
|
13
|
+
type BridgePanelModule,
|
|
14
|
+
BridgeStopSignal,
|
|
15
|
+
isBridgeLifecycleSubject,
|
|
16
|
+
isBridgeStopSignal,
|
|
17
|
+
} from "../modules";
|
|
11
18
|
import {
|
|
12
19
|
BridgeControl,
|
|
13
20
|
type BridgeField,
|
|
@@ -50,6 +57,10 @@ import { actionsOf } from "./bridgeActions";
|
|
|
50
57
|
|
|
51
58
|
const UPLOAD_MODES = new Set<string>(Object.values(BridgeUploadMode));
|
|
52
59
|
|
|
60
|
+
const READY_TIMEOUT_MIN_SECONDS = 1;
|
|
61
|
+
|
|
62
|
+
const READY_TIMEOUT_MAX_SECONDS = 3600;
|
|
63
|
+
|
|
53
64
|
const RESERVED_MODULE_IDS = new Set([
|
|
54
65
|
"announce",
|
|
55
66
|
"backup",
|
|
@@ -591,6 +602,47 @@ const validateSection = (modules: Record<string, BridgePanelModule>, section: Br
|
|
|
591
602
|
}
|
|
592
603
|
};
|
|
593
604
|
|
|
605
|
+
const validateLifecycle = (driver: BridgeDriver) => {
|
|
606
|
+
const environment: unknown = driver.lifecycle.environment;
|
|
607
|
+
|
|
608
|
+
if (environment !== undefined && typeof environment !== "function") {
|
|
609
|
+
fail(
|
|
610
|
+
"the lifecycle module declares an environment that is not a function — it must resolve the variables the process starts with",
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const stopSignal: unknown = driver.lifecycle.stopSignal;
|
|
615
|
+
|
|
616
|
+
if (stopSignal !== undefined && !isBridgeStopSignal(stopSignal)) {
|
|
617
|
+
fail(
|
|
618
|
+
`the lifecycle module declares stop signal "${String(stopSignal)}" — the bridge sends only ${Object.values(BridgeStopSignal).join(" or ")}`,
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const readyTimeoutSeconds: unknown = driver.lifecycle.readyTimeoutSeconds;
|
|
623
|
+
|
|
624
|
+
if (
|
|
625
|
+
readyTimeoutSeconds !== undefined
|
|
626
|
+
&& !(
|
|
627
|
+
Number.isInteger(readyTimeoutSeconds)
|
|
628
|
+
&& (readyTimeoutSeconds as number) >= READY_TIMEOUT_MIN_SECONDS
|
|
629
|
+
&& (readyTimeoutSeconds as number) <= READY_TIMEOUT_MAX_SECONDS
|
|
630
|
+
)
|
|
631
|
+
) {
|
|
632
|
+
fail(
|
|
633
|
+
`the lifecycle module declares a ready timeout of ${String(readyTimeoutSeconds)} seconds — it must be a whole number from ${READY_TIMEOUT_MIN_SECONDS} to ${READY_TIMEOUT_MAX_SECONDS}`,
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const subject: unknown = driver.lifecycle.subject;
|
|
638
|
+
|
|
639
|
+
if (subject !== undefined && !isBridgeLifecycleSubject(subject)) {
|
|
640
|
+
fail(
|
|
641
|
+
`the lifecycle module declares subject "${String(subject)}" — the bridge names only ${Object.values(BridgeLifecycleSubject).join(" or ")}`,
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
|
|
594
646
|
const validatePending = (driver: BridgeDriver) => {
|
|
595
647
|
const pending = driver.pending;
|
|
596
648
|
|
|
@@ -732,6 +784,7 @@ export const validateDriver = (driver: BridgeDriver) => {
|
|
|
732
784
|
|
|
733
785
|
countIn(emitted.size, BRIDGE_EVENTS_LIMIT, "event names", "the driver");
|
|
734
786
|
|
|
787
|
+
validateLifecycle(driver);
|
|
735
788
|
validatePending(driver);
|
|
736
789
|
validateSetup(driver);
|
|
737
790
|
|