@serverkgg/bridge 0.0.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/LICENSE.md +22 -0
- package/README.md +52 -0
- package/package.json +26 -0
- package/src/cli/compile.ts +75 -0
- package/src/cli/directorySource.ts +48 -0
- package/src/cli/main.ts +44 -0
- package/src/cli/validate.ts +94 -0
- package/src/codecs/index.ts +3 -0
- package/src/codecs/json.ts +42 -0
- package/src/codecs/properties.ts +75 -0
- package/src/codecs/ueIni.ts +235 -0
- package/src/context/bridgeContext.ts +120 -0
- package/src/context/index.ts +1 -0
- package/src/core/bridgeAction.ts +13 -0
- package/src/core/bridgeId.ts +3 -0
- package/src/core/bridgeKind.ts +11 -0
- package/src/core/bridgePort.ts +4 -0
- package/src/core/bridgeRuntime.ts +13 -0
- package/src/core/bridgeSecretError.ts +10 -0
- package/src/core/bridgeStream.ts +4 -0
- package/src/core/bridgeValue.ts +19 -0
- package/src/core/index.ts +8 -0
- package/src/main.ts +5 -0
- package/src/manifest/assets.ts +102 -0
- package/src/manifest/category.ts +10 -0
- package/src/manifest/checks.ts +50 -0
- package/src/manifest/dimensions.ts +106 -0
- package/src/manifest/guide.ts +36 -0
- package/src/manifest/image.ts +1 -0
- package/src/manifest/index.ts +9 -0
- package/src/manifest/localized.ts +8 -0
- package/src/manifest/manifest.ts +170 -0
- package/src/manifest/source.ts +89 -0
- package/src/modules/bridge.ts +53 -0
- package/src/modules/bridgeDriver.ts +19 -0
- package/src/modules/bridgeModule.ts +114 -0
- package/src/modules/index.ts +3 -0
- package/src/panel/bridgeConfirm.ts +5 -0
- package/src/panel/bridgeControl.ts +7 -0
- package/src/panel/bridgeFormTarget.ts +4 -0
- package/src/panel/bridgeIcon.ts +17 -0
- package/src/panel/bridgeLayout.ts +6 -0
- package/src/panel/bridgePanel.ts +107 -0
- package/src/panel/index.ts +6 -0
- package/src/probes/a2s.ts +170 -0
- package/src/probes/index.ts +1 -0
- package/src/protocol/bridgeConfig.ts +13 -0
- package/src/protocol/bridgeFrame.ts +91 -0
- package/src/protocol/bridgeLine.ts +40 -0
- package/src/protocol/bridgeManifest.ts +19 -0
- package/src/protocol/bridgeVersion.ts +11 -0
- package/src/protocol/index.ts +7 -0
- package/src/runtime/bridgeCodec.ts +78 -0
- package/src/runtime/bridgeConfigReader.ts +119 -0
- package/src/runtime/bridgeContext.ts +96 -0
- package/src/runtime/bridgeDriver.ts +179 -0
- package/src/runtime/bridgeEvents.ts +71 -0
- package/src/runtime/bridgeExec.ts +143 -0
- package/src/runtime/bridgeFiles.ts +412 -0
- package/src/runtime/bridgeLog.ts +15 -0
- package/src/runtime/bridgeNet.ts +59 -0
- package/src/runtime/bridgePath.ts +132 -0
- package/src/runtime/bridgePrivilege.ts +99 -0
- package/src/runtime/bridgeProbe.ts +14 -0
- package/src/runtime/bridgeRegistry.ts +311 -0
- package/src/runtime/bridgeServer.ts +233 -0
- package/src/runtime/bridgeState.ts +40 -0
- package/src/runtime/bridgeSupervisor.ts +498 -0
- package/src/runtime/bridgeZip.ts +122 -0
- package/src/runtime/index.ts +18 -0
- package/src/runtime/serve.ts +294 -0
- package/src/utils/glob.ts +62 -0
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { BridgeOption, LocalizedText } from "../core";
|
|
2
|
+
import type { BridgeConfirm } from "./bridgeConfirm";
|
|
3
|
+
import type { BridgeControl } from "./bridgeControl";
|
|
4
|
+
import type { BridgeFormTarget } from "./bridgeFormTarget";
|
|
5
|
+
import type { BridgeIcon } from "./bridgeIcon";
|
|
6
|
+
import type { BridgeLayout } from "./bridgeLayout";
|
|
7
|
+
|
|
8
|
+
export interface BridgeVisibleWhen {
|
|
9
|
+
variable: string;
|
|
10
|
+
values: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BridgeOptionsRef {
|
|
14
|
+
module: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface BridgeField {
|
|
18
|
+
key: string;
|
|
19
|
+
control: BridgeControl;
|
|
20
|
+
label: LocalizedText;
|
|
21
|
+
help?: LocalizedText;
|
|
22
|
+
warning?: LocalizedText;
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
min?: number;
|
|
25
|
+
max?: number;
|
|
26
|
+
step?: number;
|
|
27
|
+
maxLength?: number;
|
|
28
|
+
options?: BridgeOption[] | BridgeOptionsRef;
|
|
29
|
+
visibleWhen?: BridgeVisibleWhen;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface BridgeSectionAction {
|
|
33
|
+
id: string;
|
|
34
|
+
label: LocalizedText;
|
|
35
|
+
confirm?: BridgeConfirm;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface BridgeSectionAdd {
|
|
39
|
+
label: LocalizedText;
|
|
40
|
+
placeholder?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface BridgeFormSection {
|
|
44
|
+
layout: BridgeLayout.Form;
|
|
45
|
+
id: string;
|
|
46
|
+
title?: LocalizedText;
|
|
47
|
+
target: BridgeFormTarget;
|
|
48
|
+
module?: string;
|
|
49
|
+
reinstall?: boolean;
|
|
50
|
+
restartHint?: boolean;
|
|
51
|
+
confirm?: BridgeConfirm;
|
|
52
|
+
confirmText?: LocalizedText;
|
|
53
|
+
fields: BridgeField[];
|
|
54
|
+
visibleWhen?: BridgeVisibleWhen;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface BridgeColumn {
|
|
58
|
+
key: string;
|
|
59
|
+
label: LocalizedText;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface BridgeTableSection {
|
|
63
|
+
layout: BridgeLayout.Table;
|
|
64
|
+
id: string;
|
|
65
|
+
title?: LocalizedText;
|
|
66
|
+
module: string;
|
|
67
|
+
columns: BridgeColumn[];
|
|
68
|
+
add?: BridgeSectionAdd;
|
|
69
|
+
actions?: BridgeSectionAction[];
|
|
70
|
+
empty?: LocalizedText;
|
|
71
|
+
visibleWhen?: BridgeVisibleWhen;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface BridgeCardsSection {
|
|
75
|
+
layout: BridgeLayout.Cards;
|
|
76
|
+
id: string;
|
|
77
|
+
title?: LocalizedText;
|
|
78
|
+
module: string;
|
|
79
|
+
titleKey: string;
|
|
80
|
+
subtitleKey?: string;
|
|
81
|
+
actions?: BridgeSectionAction[];
|
|
82
|
+
empty?: LocalizedText;
|
|
83
|
+
visibleWhen?: BridgeVisibleWhen;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface BridgeCatalogSection {
|
|
87
|
+
layout: BridgeLayout.Catalog;
|
|
88
|
+
id: string;
|
|
89
|
+
title?: LocalizedText;
|
|
90
|
+
module: string;
|
|
91
|
+
restartHint?: boolean;
|
|
92
|
+
empty?: LocalizedText;
|
|
93
|
+
visibleWhen?: BridgeVisibleWhen;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type BridgeSection = BridgeCardsSection | BridgeCatalogSection | BridgeFormSection | BridgeTableSection;
|
|
97
|
+
|
|
98
|
+
export interface BridgeTab {
|
|
99
|
+
id: string;
|
|
100
|
+
title: LocalizedText;
|
|
101
|
+
icon?: BridgeIcon;
|
|
102
|
+
sections: BridgeSection[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface BridgePanel {
|
|
106
|
+
tabs: BridgeTab[];
|
|
107
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { BridgeA2sInfo } from "../context";
|
|
2
|
+
|
|
3
|
+
const SIMPLE_PREFIX = 0xff;
|
|
4
|
+
|
|
5
|
+
const INFO_HEADER = 0x49;
|
|
6
|
+
|
|
7
|
+
const CHALLENGE_HEADER = 0x41;
|
|
8
|
+
|
|
9
|
+
const INFO_PAYLOAD = new TextEncoder().encode("Source Engine Query\0");
|
|
10
|
+
|
|
11
|
+
const DECODER = new TextDecoder();
|
|
12
|
+
|
|
13
|
+
const infoRequest = (challenge: Uint8Array | null) => {
|
|
14
|
+
const request = new Uint8Array(5 + INFO_PAYLOAD.length + (challenge?.length ?? 0));
|
|
15
|
+
|
|
16
|
+
request.set([
|
|
17
|
+
SIMPLE_PREFIX,
|
|
18
|
+
SIMPLE_PREFIX,
|
|
19
|
+
SIMPLE_PREFIX,
|
|
20
|
+
SIMPLE_PREFIX,
|
|
21
|
+
0x54,
|
|
22
|
+
]);
|
|
23
|
+
request.set(INFO_PAYLOAD, 5);
|
|
24
|
+
|
|
25
|
+
if (challenge) {
|
|
26
|
+
request.set(challenge, 5 + INFO_PAYLOAD.length);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return request;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const readCString = (view: Uint8Array, offset: number) => {
|
|
33
|
+
let index = offset;
|
|
34
|
+
|
|
35
|
+
while (index < view.length && view[index] !== 0) {
|
|
36
|
+
index += 1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
value: DECODER.decode(view.subarray(offset, index)),
|
|
41
|
+
next: index + 1,
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const parseInfo = (view: Uint8Array) => {
|
|
46
|
+
const name = readCString(view, 6);
|
|
47
|
+
const map = readCString(view, name.next);
|
|
48
|
+
const folder = readCString(view, map.next);
|
|
49
|
+
const game = readCString(view, folder.next);
|
|
50
|
+
|
|
51
|
+
const offset = game.next + 2;
|
|
52
|
+
|
|
53
|
+
const online = view[offset];
|
|
54
|
+
const max = view[offset + 1];
|
|
55
|
+
|
|
56
|
+
if (online === undefined || max === undefined) {
|
|
57
|
+
throw new Error("a2s info response is truncated");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
players: {
|
|
62
|
+
online,
|
|
63
|
+
max,
|
|
64
|
+
},
|
|
65
|
+
name: name.value.length > 0 ? name.value : null,
|
|
66
|
+
map: map.value.length > 0 ? map.value : null,
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const probeA2s = (host: string, hostPort: number, timeoutMs: number) => {
|
|
71
|
+
return new Promise<BridgeA2sInfo>((resolve, reject) => {
|
|
72
|
+
const startedAt = performance.now();
|
|
73
|
+
|
|
74
|
+
let socket: Awaited<ReturnType<typeof Bun.udpSocket>> | null = null;
|
|
75
|
+
let challenged = false;
|
|
76
|
+
let settled = false;
|
|
77
|
+
|
|
78
|
+
const finish = (outcome: Omit<BridgeA2sInfo, "latencyMs"> | Error) => {
|
|
79
|
+
if (settled) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
settled = true;
|
|
84
|
+
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
socket?.close();
|
|
87
|
+
|
|
88
|
+
if (outcome instanceof Error) {
|
|
89
|
+
reject(outcome);
|
|
90
|
+
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
resolve({
|
|
95
|
+
...outcome,
|
|
96
|
+
latencyMs: Math.round(performance.now() - startedAt),
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const timer = setTimeout(() => {
|
|
101
|
+
finish(new Error("a2s query timed out"));
|
|
102
|
+
}, timeoutMs);
|
|
103
|
+
|
|
104
|
+
const handle = (view: Uint8Array) => {
|
|
105
|
+
if (
|
|
106
|
+
view.length < 5
|
|
107
|
+
|| view[0] !== SIMPLE_PREFIX
|
|
108
|
+
|| view[1] !== SIMPLE_PREFIX
|
|
109
|
+
|| view[2] !== SIMPLE_PREFIX
|
|
110
|
+
|| view[3] !== SIMPLE_PREFIX
|
|
111
|
+
) {
|
|
112
|
+
finish(new Error("a2s response is malformed"));
|
|
113
|
+
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (view[4] === CHALLENGE_HEADER) {
|
|
118
|
+
if (challenged || view.length < 9) {
|
|
119
|
+
finish(new Error("a2s challenge is malformed"));
|
|
120
|
+
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
challenged = true;
|
|
125
|
+
|
|
126
|
+
socket?.send(infoRequest(view.slice(5, 9)));
|
|
127
|
+
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (view[4] !== INFO_HEADER) {
|
|
132
|
+
finish(new Error("a2s response has an unexpected header"));
|
|
133
|
+
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
finish(parseInfo(view));
|
|
139
|
+
} catch (error) {
|
|
140
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
Bun.udpSocket({
|
|
145
|
+
connect: {
|
|
146
|
+
hostname: host,
|
|
147
|
+
port: hostPort,
|
|
148
|
+
},
|
|
149
|
+
socket: {
|
|
150
|
+
data(_socket, data) {
|
|
151
|
+
handle(data);
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
.then((created) => {
|
|
156
|
+
if (settled) {
|
|
157
|
+
created.close();
|
|
158
|
+
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
socket = created;
|
|
163
|
+
|
|
164
|
+
created.send(infoRequest(null));
|
|
165
|
+
})
|
|
166
|
+
.catch((error) => {
|
|
167
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./a2s";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface BridgeConfig {
|
|
2
|
+
code: string;
|
|
3
|
+
token: string;
|
|
4
|
+
cwd: string;
|
|
5
|
+
run: string;
|
|
6
|
+
ports: Record<string, number>;
|
|
7
|
+
memoryMb: number;
|
|
8
|
+
cpuPercent: number;
|
|
9
|
+
gameUid: number;
|
|
10
|
+
gameGid: number;
|
|
11
|
+
variables: Record<string, string>;
|
|
12
|
+
secrets: Record<string, string>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { BridgeDesired, BridgeRuntime, BridgeValues } from "../core";
|
|
2
|
+
|
|
3
|
+
export const BRIDGE_SOCKET_NAME = "bridge.sock";
|
|
4
|
+
|
|
5
|
+
export const BRIDGE_CONFIG_NAME = "config.json";
|
|
6
|
+
|
|
7
|
+
export const BRIDGE_STATE_NAME = "state.json";
|
|
8
|
+
|
|
9
|
+
export const BRIDGE_RUN_MOUNT = "/serverk/run";
|
|
10
|
+
|
|
11
|
+
export const BRIDGE_FRAME_LIMIT = 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
export const BRIDGE_INSTALL_ERROR_LIMIT = 2048;
|
|
14
|
+
|
|
15
|
+
export enum BridgeMethod {
|
|
16
|
+
Command = "command",
|
|
17
|
+
Events = "events",
|
|
18
|
+
Invoke = "invoke",
|
|
19
|
+
Manifest = "manifest",
|
|
20
|
+
Quiesce = "quiesce",
|
|
21
|
+
Release = "release",
|
|
22
|
+
Sample = "sample",
|
|
23
|
+
Start = "start",
|
|
24
|
+
Status = "status",
|
|
25
|
+
Stop = "stop",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface BridgeRequest {
|
|
29
|
+
id: number;
|
|
30
|
+
token: string;
|
|
31
|
+
method: BridgeMethod;
|
|
32
|
+
input?: string;
|
|
33
|
+
expect?: string;
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
module?: string;
|
|
36
|
+
action?: string;
|
|
37
|
+
args?: BridgeValues;
|
|
38
|
+
cursor?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface BridgeCommandResult {
|
|
42
|
+
sent: string;
|
|
43
|
+
line: string | null;
|
|
44
|
+
groups: Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface BridgeSuccess {
|
|
48
|
+
id: number | null;
|
|
49
|
+
ok: true;
|
|
50
|
+
result: unknown;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface BridgeFailure {
|
|
54
|
+
id: number | null;
|
|
55
|
+
ok: false;
|
|
56
|
+
error: string;
|
|
57
|
+
secret?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type BridgeResponse = BridgeFailure | BridgeSuccess;
|
|
61
|
+
|
|
62
|
+
export interface BridgeStatus {
|
|
63
|
+
version: string;
|
|
64
|
+
runtime: BridgeRuntime;
|
|
65
|
+
desired: BridgeDesired;
|
|
66
|
+
exitCode: number | null;
|
|
67
|
+
restarts: number;
|
|
68
|
+
installError: string | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface BridgeSampleResult {
|
|
72
|
+
online: number | null;
|
|
73
|
+
max: number | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface BridgeQuiesceResult {
|
|
77
|
+
supported: boolean;
|
|
78
|
+
quiesced: boolean;
|
|
79
|
+
settleSeconds: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface BridgeEvent {
|
|
83
|
+
seq: number;
|
|
84
|
+
event: string;
|
|
85
|
+
payload: BridgeValues | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface BridgeEventsResult {
|
|
89
|
+
events: BridgeEvent[];
|
|
90
|
+
cursor: number;
|
|
91
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const BRIDGE_LOG_SOURCE = "bridge";
|
|
2
|
+
|
|
3
|
+
export interface BridgeLogLine {
|
|
4
|
+
message: string;
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const parseBridgeLine = (line: string): BridgeLogLine | null => {
|
|
9
|
+
if (!line.startsWith("{") || !line.endsWith("}")) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let parsed: unknown;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
parsed = JSON.parse(line);
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const entry = parsed as Record<string, unknown>;
|
|
26
|
+
|
|
27
|
+
if (entry.source !== BRIDGE_LOG_SOURCE || typeof entry.message !== "string") {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const fields = Object.entries(entry)
|
|
32
|
+
.filter(([key]) => key !== "source" && key !== "message")
|
|
33
|
+
.map(([key, value]) => `${key}=${typeof value === "object" ? JSON.stringify(value) : String(value)}`)
|
|
34
|
+
.join(" ");
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
message: entry.message,
|
|
38
|
+
text: fields.length > 0 ? `${entry.message} ${fields}` : entry.message,
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BridgeKind } from "../core";
|
|
2
|
+
import type { BridgePanel } from "../panel";
|
|
3
|
+
|
|
4
|
+
export interface BridgeModuleManifest {
|
|
5
|
+
id: string;
|
|
6
|
+
kind: BridgeKind;
|
|
7
|
+
requiresRunning: boolean;
|
|
8
|
+
refreshSeconds: number | null;
|
|
9
|
+
pageSize: number | null;
|
|
10
|
+
dependsOn: string[];
|
|
11
|
+
actions: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface BridgeManifest {
|
|
15
|
+
version: string;
|
|
16
|
+
panel: BridgePanel | null;
|
|
17
|
+
modules: BridgeModuleManifest[];
|
|
18
|
+
events: string[];
|
|
19
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import pkg from "../../package.json";
|
|
2
|
+
|
|
3
|
+
export const BRIDGE_VERSION: string = pkg.version;
|
|
4
|
+
|
|
5
|
+
export const BRIDGE_MAJOR: number = Number(BRIDGE_VERSION.split(".").at(0));
|
|
6
|
+
|
|
7
|
+
if (!Number.isInteger(BRIDGE_MAJOR) || BRIDGE_MAJOR < 0) {
|
|
8
|
+
throw new Error(`the bridge version "${BRIDGE_VERSION}" has no valid major`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const BRIDGE_VERSION_LABEL = "gg.serverk.bridge.version";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { mergeJson, mergeProperties, mergeUeIni, parseJson, parseProperties, parseUeIni } from "../codecs";
|
|
2
|
+
import type { BridgeCodec, BridgeCodecOptions, BridgeCodecs, BridgeFiles } from "../context";
|
|
3
|
+
import type { BridgeValue, BridgeValues } from "../core";
|
|
4
|
+
|
|
5
|
+
const toText = (value: BridgeValue) => {
|
|
6
|
+
return value === null ? "" : String(value);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const toStrings = (values: BridgeValues) => {
|
|
10
|
+
const out: Record<string, string> = {};
|
|
11
|
+
|
|
12
|
+
for (const [key, value] of Object.entries(values)) {
|
|
13
|
+
out[key] = toText(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return out;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const widen = (values: Record<string, string>): BridgeValues => {
|
|
20
|
+
return values;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const readOr = async (files: BridgeFiles, path: string) => {
|
|
24
|
+
return (await files.exists(path)) ? await files.read(path) : "";
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const requireTarget = (options: BridgeCodecOptions | undefined, path: string) => {
|
|
28
|
+
const section = options?.section;
|
|
29
|
+
const object = options?.object;
|
|
30
|
+
|
|
31
|
+
if (!section || !object) {
|
|
32
|
+
throw new Error(`the ue-ini codec needs "section" and "object" options to edit "${path}"`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
section,
|
|
37
|
+
object,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const createBridgeCodecs = (files: BridgeFiles): BridgeCodecs => {
|
|
42
|
+
const properties: BridgeCodec = {
|
|
43
|
+
async read(path) {
|
|
44
|
+
return widen(parseProperties(await readOr(files, path)));
|
|
45
|
+
},
|
|
46
|
+
async merge(path, values) {
|
|
47
|
+
await files.write(path, mergeProperties(await readOr(files, path), toStrings(values)));
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const json: BridgeCodec = {
|
|
52
|
+
async read(path) {
|
|
53
|
+
return parseJson(await readOr(files, path));
|
|
54
|
+
},
|
|
55
|
+
async merge(path, values) {
|
|
56
|
+
await files.write(path, mergeJson(await readOr(files, path), values));
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const ueIni: BridgeCodec = {
|
|
61
|
+
async read(path, options) {
|
|
62
|
+
const target = requireTarget(options, path);
|
|
63
|
+
|
|
64
|
+
return widen(parseUeIni(await readOr(files, path), target.section, target.object));
|
|
65
|
+
},
|
|
66
|
+
async merge(path, values, options) {
|
|
67
|
+
const target = requireTarget(options, path);
|
|
68
|
+
|
|
69
|
+
await files.write(path, mergeUeIni(await readOr(files, path), target.section, target.object, toStrings(values)));
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
properties,
|
|
75
|
+
json,
|
|
76
|
+
ueIni,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BRIDGE_CONFIG_NAME,
|
|
3
|
+
BRIDGE_RUN_MOUNT,
|
|
4
|
+
BRIDGE_SOCKET_NAME,
|
|
5
|
+
BRIDGE_STATE_NAME,
|
|
6
|
+
type BridgeConfig,
|
|
7
|
+
} from "../protocol";
|
|
8
|
+
|
|
9
|
+
export class BridgeConfigError extends Error {
|
|
10
|
+
constructor(message: string) {
|
|
11
|
+
super(message);
|
|
12
|
+
|
|
13
|
+
this.name = "BridgeConfigError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const requireString = (source: Record<string, unknown>, key: string) => {
|
|
18
|
+
const value = source[key];
|
|
19
|
+
|
|
20
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
21
|
+
throw new BridgeConfigError(`config field "${key}" must be a non-empty string`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return value;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const requireNumber = (source: Record<string, unknown>, key: string) => {
|
|
28
|
+
const value = source[key];
|
|
29
|
+
|
|
30
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
31
|
+
throw new BridgeConfigError(`config field "${key}" must be a number`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return value;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const numberRecord = (source: Record<string, unknown>, key: string) => {
|
|
38
|
+
const value = source[key];
|
|
39
|
+
|
|
40
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const out: Record<string, number> = {};
|
|
45
|
+
|
|
46
|
+
for (const [entryKey, entryValue] of Object.entries(value as Record<string, unknown>)) {
|
|
47
|
+
if (typeof entryValue === "number") {
|
|
48
|
+
out[entryKey] = entryValue;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return out;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const stringRecord = (source: Record<string, unknown>, key: string) => {
|
|
56
|
+
const value = source[key];
|
|
57
|
+
|
|
58
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const out: Record<string, string> = {};
|
|
63
|
+
|
|
64
|
+
for (const [entryKey, entryValue] of Object.entries(value as Record<string, unknown>)) {
|
|
65
|
+
if (typeof entryValue === "string") {
|
|
66
|
+
out[entryKey] = entryValue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return out;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const configPath = () => {
|
|
74
|
+
return `${BRIDGE_RUN_MOUNT}/${BRIDGE_CONFIG_NAME}`;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const socketPath = (config: BridgeConfig) => {
|
|
78
|
+
return `${config.run}/${BRIDGE_SOCKET_NAME}`;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const statePath = (config: BridgeConfig) => {
|
|
82
|
+
return `${config.run}/${BRIDGE_STATE_NAME}`;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const readConfig = async (path: string): Promise<BridgeConfig> => {
|
|
86
|
+
const file = Bun.file(path);
|
|
87
|
+
|
|
88
|
+
if (!(await file.exists())) {
|
|
89
|
+
throw new BridgeConfigError(`no bridge config at "${path}"`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const parsed: unknown = JSON.parse(await file.text());
|
|
93
|
+
|
|
94
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
95
|
+
throw new BridgeConfigError("bridge config must be an object");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const source = parsed as Record<string, unknown>;
|
|
99
|
+
|
|
100
|
+
const config: BridgeConfig = {
|
|
101
|
+
code: requireString(source, "code"),
|
|
102
|
+
token: requireString(source, "token"),
|
|
103
|
+
cwd: requireString(source, "cwd"),
|
|
104
|
+
run: requireString(source, "run"),
|
|
105
|
+
ports: numberRecord(source, "ports"),
|
|
106
|
+
memoryMb: requireNumber(source, "memoryMb"),
|
|
107
|
+
cpuPercent: requireNumber(source, "cpuPercent"),
|
|
108
|
+
gameUid: requireNumber(source, "gameUid"),
|
|
109
|
+
gameGid: requireNumber(source, "gameGid"),
|
|
110
|
+
variables: stringRecord(source, "variables"),
|
|
111
|
+
secrets: stringRecord(source, "secrets"),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (config.gameUid === 0 || config.gameGid === 0) {
|
|
115
|
+
throw new BridgeConfigError("the game must not run as root");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return config;
|
|
119
|
+
};
|