@serverkgg/bridge 0.0.12 → 0.0.13
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 +9 -1
- package/src/catalogs/curseforge.test.ts +580 -0
- package/src/catalogs/curseforge.ts +461 -0
- package/src/catalogs/index.ts +2 -0
- package/src/catalogs/thunderstore.test.ts +94 -0
- package/src/catalogs/thunderstore.ts +239 -0
- package/src/codecs/index.ts +3 -0
- package/src/codecs/ini.test.ts +152 -0
- package/src/codecs/ini.ts +158 -0
- package/src/codecs/sourceCfg.test.ts +100 -0
- package/src/codecs/sourceCfg.ts +111 -0
- package/src/codecs/xmlProperties.test.ts +104 -0
- package/src/codecs/xmlProperties.ts +133 -0
- package/src/context/bridgeContext.ts +20 -0
- package/src/core/bridgeRconError.ts +19 -0
- package/src/core/index.ts +1 -0
- package/src/install/index.ts +1 -0
- package/src/install/installStamp.test.ts +125 -0
- package/src/install/installStamp.ts +40 -0
- package/src/main.ts +1 -0
- package/src/manifest/index.ts +1 -0
- package/src/manifest/manifest.test.ts +427 -2
- package/src/manifest/manifest.ts +141 -13
- package/src/manifest/primitives.ts +6 -0
- package/src/manifest/runtime.ts +89 -0
- package/src/net/index.ts +2 -0
- package/src/net/loopback.test.ts +152 -0
- package/src/net/loopback.ts +109 -0
- package/src/net/loopbackMethod.ts +7 -0
- package/src/presence/index.ts +1 -0
- package/src/presence/rosterSync.test.ts +179 -0
- package/src/presence/rosterSync.ts +57 -0
- package/src/protocol/bridgeEvents.ts +4 -0
- package/src/rcon/index.ts +3 -0
- package/src/rcon/sourceRcon.test.ts +308 -0
- package/src/rcon/sourceRcon.ts +276 -0
- package/src/rcon/sourceRconFrame.test.ts +152 -0
- package/src/rcon/sourceRconFrame.ts +99 -0
- package/src/runtime/bridgeCodec.ts +45 -2
- package/src/runtime/bridgeContext.ts +13 -6
- package/src/runtime/bridgeDriver.test.ts +1 -0
- package/src/runtime/bridgeLiveConfig.test.ts +149 -0
- package/src/runtime/bridgeLiveConfig.ts +63 -0
- package/src/runtime/bridgeRcon.ts +15 -0
- package/src/runtime/bridgeRegistry.ts +1 -0
- package/src/steam/index.ts +7 -0
- package/src/steam/steamApp.test.ts +78 -0
- package/src/steam/steamApp.ts +35 -0
- package/src/steam/steamcmd.test.ts +157 -0
- package/src/steam/steamcmd.ts +273 -0
- package/src/steam/steamcmdLogin.ts +34 -0
- package/src/steam/steamcmdNarrator.test.ts +90 -0
- package/src/steam/steamcmdNarrator.ts +43 -0
- package/src/steam/steamcmdPlatform.ts +4 -0
- package/src/steam/webApi.ts +70 -0
- package/src/steam/workshop.test.ts +45 -0
- package/src/steam/workshop.ts +148 -0
- package/src/utils/execDetail.ts +16 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/token.test.ts +50 -0
- package/src/utils/token.ts +35 -0
- package/src/wine/index.ts +3 -0
- package/src/wine/protonCommand.test.ts +67 -0
- package/src/wine/protonCommand.ts +49 -0
- package/src/wine/wineCommand.test.ts +92 -0
- package/src/wine/wineCommand.ts +54 -0
- package/src/wine/winePrefix.test.ts +111 -0
- package/src/wine/winePrefix.ts +88 -0
- package/src/runtime/bridgeSecrets.test.ts +0 -121
- package/src/runtime/bridgeSecrets.ts +0 -43
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { BridgeValue, BridgeValues } from "../core";
|
|
2
|
+
|
|
3
|
+
const ENTRY = /^(?<key>[A-Za-z_][A-Za-z0-9_.]*)(?:[ \t]+(?<value>.*))?$/;
|
|
4
|
+
|
|
5
|
+
const NUMERIC = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/;
|
|
6
|
+
|
|
7
|
+
const NEWLINES = /[\r\n]+/g;
|
|
8
|
+
|
|
9
|
+
export interface SourceCfgEntry {
|
|
10
|
+
key: string;
|
|
11
|
+
value: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const readSourceCfgEntry = (line: string): SourceCfgEntry | null => {
|
|
15
|
+
const trimmed = line.trim();
|
|
16
|
+
|
|
17
|
+
if (trimmed.length === 0 || trimmed.startsWith("//") || trimmed.startsWith("#")) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const match = trimmed.match(ENTRY);
|
|
22
|
+
|
|
23
|
+
if (!match?.groups?.key) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
key: match.groups.key,
|
|
29
|
+
value: (match.groups.value ?? "").trim(),
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const decodeSourceCfgValue = (raw: string): BridgeValue => {
|
|
34
|
+
if (raw.length >= 2 && raw.startsWith('"') && raw.endsWith('"')) {
|
|
35
|
+
return raw.slice(1, -1).replaceAll('\\"', '"').replaceAll("\\\\", "\\");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (raw === "true" || raw === "false") {
|
|
39
|
+
return raw === "true";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (NUMERIC.test(raw)) {
|
|
43
|
+
return Number(raw);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return raw;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const encodeSourceCfgValue = (value: BridgeValue): string => {
|
|
50
|
+
if (typeof value === "boolean") {
|
|
51
|
+
return value ? "true" : "false";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (typeof value === "number") {
|
|
55
|
+
return Number.isFinite(value) ? String(value) : '""';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const text = (value ?? "").replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll(NEWLINES, " ");
|
|
59
|
+
|
|
60
|
+
return `"${text}"`;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const parseSourceCfg = (contents: string): BridgeValues => {
|
|
64
|
+
const values: BridgeValues = {};
|
|
65
|
+
|
|
66
|
+
for (const line of contents.split("\n")) {
|
|
67
|
+
const entry = readSourceCfgEntry(line);
|
|
68
|
+
|
|
69
|
+
if (entry) {
|
|
70
|
+
values[entry.key] = decodeSourceCfgValue(entry.value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return values;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const mergeSourceCfg = (contents: string, values: BridgeValues): string => {
|
|
78
|
+
const pending = new Map(Object.entries(values));
|
|
79
|
+
const written = new Set<string>();
|
|
80
|
+
const lines: string[] = [];
|
|
81
|
+
|
|
82
|
+
for (const line of contents.split("\n")) {
|
|
83
|
+
const entry = readSourceCfgEntry(line);
|
|
84
|
+
const key = entry?.key ?? "";
|
|
85
|
+
|
|
86
|
+
if (!entry || !pending.has(key)) {
|
|
87
|
+
lines.push(line);
|
|
88
|
+
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (written.has(key)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
written.add(key);
|
|
97
|
+
lines.push(`${key} ${encodeSourceCfgValue(pending.get(key) ?? null)}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
while (lines.length > 0 && (lines.at(-1) ?? "").trim().length === 0) {
|
|
101
|
+
lines.pop();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const [key, value] of pending) {
|
|
105
|
+
if (!written.has(key)) {
|
|
106
|
+
lines.push(`${key} ${encodeSourceCfgValue(value)}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return `${lines.join("\n")}\n`;
|
|
111
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mergeXmlProperties, parseXmlProperties } from "./xmlProperties";
|
|
3
|
+
|
|
4
|
+
const SAMPLE = [
|
|
5
|
+
'<?xml version="1.0"?>',
|
|
6
|
+
"<ServerSettings>",
|
|
7
|
+
'\t<property name="ServerName" value="serverk"/>',
|
|
8
|
+
"\t<!-- a note -->",
|
|
9
|
+
'\t<property name="ServerPort" value="26900"/>',
|
|
10
|
+
'\t<property value="8" name="ServerMaxPlayerCount"/>',
|
|
11
|
+
"</ServerSettings>",
|
|
12
|
+
"",
|
|
13
|
+
].join("\n");
|
|
14
|
+
|
|
15
|
+
describe("parseXmlProperties", () => {
|
|
16
|
+
test("reads every property whichever way the attributes are ordered", () => {
|
|
17
|
+
expect(parseXmlProperties(SAMPLE)).toEqual({
|
|
18
|
+
ServerName: "serverk",
|
|
19
|
+
ServerPort: "26900",
|
|
20
|
+
ServerMaxPlayerCount: "8",
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("decodes entities", () => {
|
|
25
|
+
expect(parseXmlProperties('<property name="ServerName" value="a & b <c>"/>')).toEqual({
|
|
26
|
+
ServerName: "a & b <c>",
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("accepts single quoted attributes and an empty value", () => {
|
|
31
|
+
expect(parseXmlProperties("<property name='ServerPassword' value=''/>")).toEqual({
|
|
32
|
+
ServerPassword: "",
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("ignores everything that is not a property", () => {
|
|
37
|
+
expect(parseXmlProperties('<ServerSettings>\n<other name="x" value="1"/>\n</ServerSettings>')).toEqual({});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("mergeXmlProperties", () => {
|
|
42
|
+
test("rewrites a value in place and keeps order, comments and the header", () => {
|
|
43
|
+
expect(
|
|
44
|
+
mergeXmlProperties(SAMPLE, {
|
|
45
|
+
ServerPort: "26950",
|
|
46
|
+
}),
|
|
47
|
+
).toBe(
|
|
48
|
+
[
|
|
49
|
+
'<?xml version="1.0"?>',
|
|
50
|
+
"<ServerSettings>",
|
|
51
|
+
'\t<property name="ServerName" value="serverk"/>',
|
|
52
|
+
"\t<!-- a note -->",
|
|
53
|
+
'\t<property name="ServerPort" value="26950"/>',
|
|
54
|
+
'\t<property value="8" name="ServerMaxPlayerCount"/>',
|
|
55
|
+
"</ServerSettings>",
|
|
56
|
+
"",
|
|
57
|
+
].join("\n"),
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("rewrites a property whose value attribute comes first", () => {
|
|
62
|
+
expect(
|
|
63
|
+
mergeXmlProperties(SAMPLE, {
|
|
64
|
+
ServerMaxPlayerCount: "16",
|
|
65
|
+
}),
|
|
66
|
+
).toContain('<property value="16" name="ServerMaxPlayerCount"/>');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("adds an unknown property before the closing tag with the same indent", () => {
|
|
70
|
+
expect(
|
|
71
|
+
mergeXmlProperties(SAMPLE, {
|
|
72
|
+
ServerPassword: "1234",
|
|
73
|
+
}),
|
|
74
|
+
).toBe(
|
|
75
|
+
[
|
|
76
|
+
'<?xml version="1.0"?>',
|
|
77
|
+
"<ServerSettings>",
|
|
78
|
+
'\t<property name="ServerName" value="serverk"/>',
|
|
79
|
+
"\t<!-- a note -->",
|
|
80
|
+
'\t<property name="ServerPort" value="26900"/>',
|
|
81
|
+
'\t<property value="8" name="ServerMaxPlayerCount"/>',
|
|
82
|
+
'\t<property name="ServerPassword" value="1234"/>',
|
|
83
|
+
"</ServerSettings>",
|
|
84
|
+
"",
|
|
85
|
+
].join("\n"),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("escapes a value it writes", () => {
|
|
90
|
+
expect(
|
|
91
|
+
mergeXmlProperties(SAMPLE, {
|
|
92
|
+
ServerName: 'a & b "c"',
|
|
93
|
+
}),
|
|
94
|
+
).toContain('<property name="ServerName" value="a & b "c""/>');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("adds a value attribute to a property that has none", () => {
|
|
98
|
+
expect(
|
|
99
|
+
mergeXmlProperties('<property name="ServerName"/>', {
|
|
100
|
+
ServerName: "x",
|
|
101
|
+
}),
|
|
102
|
+
).toBe('<property name="ServerName" value="x"/>');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const PROPERTY_TAG = /<property\b[^>]*>/gi;
|
|
2
|
+
|
|
3
|
+
const PROPERTY_LINE = /<property\b/i;
|
|
4
|
+
|
|
5
|
+
const CLOSING_TAG = /<\/[A-Za-z_][\w.:-]*\s*>/g;
|
|
6
|
+
|
|
7
|
+
const INDENT = /^[ \t]*/;
|
|
8
|
+
|
|
9
|
+
const DEFAULT_INDENT = "\t";
|
|
10
|
+
|
|
11
|
+
const ENTITIES: Record<string, string> = {
|
|
12
|
+
amp: "&",
|
|
13
|
+
apos: "'",
|
|
14
|
+
gt: ">",
|
|
15
|
+
lt: "<",
|
|
16
|
+
quot: '"',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const ENTITY = /&(amp|apos|gt|lt|quot);/g;
|
|
20
|
+
|
|
21
|
+
const attributePattern = (name: string) => {
|
|
22
|
+
return new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, "i");
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const NAME_ATTRIBUTE = attributePattern("name");
|
|
26
|
+
|
|
27
|
+
const VALUE_ATTRIBUTE = attributePattern("value");
|
|
28
|
+
|
|
29
|
+
const decodeXml = (raw: string) => {
|
|
30
|
+
return raw.replace(ENTITY, (_match, entity: string) => ENTITIES[entity] ?? _match);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const encodeXml = (raw: string) => {
|
|
34
|
+
return raw
|
|
35
|
+
.replaceAll("&", "&")
|
|
36
|
+
.replaceAll("<", "<")
|
|
37
|
+
.replaceAll(">", ">")
|
|
38
|
+
.replaceAll('"', """)
|
|
39
|
+
.replaceAll("'", "'");
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const attributeOf = (tag: string, pattern: RegExp) => {
|
|
43
|
+
const match = tag.match(pattern);
|
|
44
|
+
|
|
45
|
+
if (!match) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return decodeXml(match[1] ?? match[2] ?? "");
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const propertyLine = (indent: string, key: string, value: string) => {
|
|
53
|
+
return `${indent}<property name="${encodeXml(key)}" value="${encodeXml(value)}"/>`;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const existingIndent = (contents: string) => {
|
|
57
|
+
for (const line of contents.split("\n")) {
|
|
58
|
+
if (PROPERTY_LINE.test(line)) {
|
|
59
|
+
return line.match(INDENT)?.at(0) ?? DEFAULT_INDENT;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return DEFAULT_INDENT;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const parseXmlProperties = (contents: string) => {
|
|
67
|
+
const values: Record<string, string> = {};
|
|
68
|
+
|
|
69
|
+
for (const [tag] of contents.matchAll(PROPERTY_TAG)) {
|
|
70
|
+
const key = attributeOf(tag, NAME_ATTRIBUTE);
|
|
71
|
+
|
|
72
|
+
if (key !== null && key.length > 0) {
|
|
73
|
+
values[key] = attributeOf(tag, VALUE_ATTRIBUTE) ?? "";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return values;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const mergeXmlProperties = (contents: string, values: Record<string, string>) => {
|
|
81
|
+
const pending = new Map(Object.entries(values));
|
|
82
|
+
const indent = existingIndent(contents);
|
|
83
|
+
|
|
84
|
+
const rewritten = contents.replace(PROPERTY_TAG, (tag) => {
|
|
85
|
+
const key = attributeOf(tag, NAME_ATTRIBUTE);
|
|
86
|
+
|
|
87
|
+
if (key === null || !pending.has(key)) {
|
|
88
|
+
return tag;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const value = encodeXml(pending.get(key) ?? "");
|
|
92
|
+
|
|
93
|
+
pending.delete(key);
|
|
94
|
+
|
|
95
|
+
if (VALUE_ATTRIBUTE.test(tag)) {
|
|
96
|
+
return tag.replace(VALUE_ATTRIBUTE, `value="${value}"`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return tag.replace(/\s*\/?>$/, ` value="${value}"/>`);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (pending.size === 0) {
|
|
103
|
+
return rewritten;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const added = [
|
|
107
|
+
...pending,
|
|
108
|
+
].map(([key, value]) => propertyLine(indent, key, value));
|
|
109
|
+
|
|
110
|
+
const closings = [
|
|
111
|
+
...rewritten.matchAll(CLOSING_TAG),
|
|
112
|
+
];
|
|
113
|
+
const last = closings.at(-1);
|
|
114
|
+
|
|
115
|
+
if (last?.index === undefined) {
|
|
116
|
+
const lines = rewritten.split("\n");
|
|
117
|
+
|
|
118
|
+
while (lines.length > 0 && (lines.at(-1) ?? "").trim().length === 0) {
|
|
119
|
+
lines.pop();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return `${[
|
|
123
|
+
...lines,
|
|
124
|
+
...added,
|
|
125
|
+
].join("\n")}\n`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const head = rewritten.slice(0, last.index);
|
|
129
|
+
const tail = rewritten.slice(last.index);
|
|
130
|
+
const lineStart = head.lastIndexOf("\n") + 1;
|
|
131
|
+
|
|
132
|
+
return `${head.slice(0, lineStart)}${added.join("\n")}\n${head.slice(lineStart)}${tail}`;
|
|
133
|
+
};
|
|
@@ -59,9 +59,12 @@ export interface BridgeCodec {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export interface BridgeCodecs {
|
|
62
|
+
ini: BridgeCodec;
|
|
62
63
|
json: BridgeCodec;
|
|
63
64
|
properties: BridgeCodec;
|
|
65
|
+
sourceCfg: BridgeCodec;
|
|
64
66
|
ueIni: BridgeCodec;
|
|
67
|
+
xmlProperties: BridgeCodec;
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
export interface BridgeRequestOptions {
|
|
@@ -115,6 +118,22 @@ export interface BridgeProbes {
|
|
|
115
118
|
raknetPing(port: number): Promise<BridgeRaknetPingStatus>;
|
|
116
119
|
}
|
|
117
120
|
|
|
121
|
+
export interface BridgeRconOptions {
|
|
122
|
+
port: number;
|
|
123
|
+
password: string;
|
|
124
|
+
timeoutMs?: number;
|
|
125
|
+
multiPacket?: boolean;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface BridgeRconClient {
|
|
129
|
+
command(text: string): Promise<string>;
|
|
130
|
+
close(): void;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface BridgeRcons {
|
|
134
|
+
source(options: BridgeRconOptions): Promise<BridgeRconClient>;
|
|
135
|
+
}
|
|
136
|
+
|
|
118
137
|
export interface BridgeCommandOptions {
|
|
119
138
|
expect?: RegExp;
|
|
120
139
|
timeoutMs?: number;
|
|
@@ -164,6 +183,7 @@ export interface BridgeContext {
|
|
|
164
183
|
codec: BridgeCodecs;
|
|
165
184
|
net: BridgeNet;
|
|
166
185
|
probe: BridgeProbes;
|
|
186
|
+
rcon: BridgeRcons;
|
|
167
187
|
command(input: string, options?: BridgeCommandOptions): Promise<BridgeReply>;
|
|
168
188
|
exec(command: string[], options?: BridgeExecOptions): Promise<BridgeExecResult>;
|
|
169
189
|
logs: BridgeLogs;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export enum BridgeRconCode {
|
|
2
|
+
AuthFailed = "auth-failed",
|
|
3
|
+
Closed = "closed",
|
|
4
|
+
Malformed = "malformed",
|
|
5
|
+
Oversized = "oversized",
|
|
6
|
+
TimedOut = "timed-out",
|
|
7
|
+
Unreachable = "unreachable",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class BridgeRconError extends Error {
|
|
11
|
+
readonly code: BridgeRconCode;
|
|
12
|
+
|
|
13
|
+
constructor(code: BridgeRconCode, message: string) {
|
|
14
|
+
super(message);
|
|
15
|
+
|
|
16
|
+
this.name = "BridgeRconError";
|
|
17
|
+
this.code = code;
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/core/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from "./bridgeKind";
|
|
|
7
7
|
export * from "./bridgeNetError";
|
|
8
8
|
export * from "./bridgePort";
|
|
9
9
|
export * from "./bridgeProbeError";
|
|
10
|
+
export * from "./bridgeRconError";
|
|
10
11
|
export * from "./bridgeRuntime";
|
|
11
12
|
export * from "./bridgeSecretError";
|
|
12
13
|
export * from "./bridgeSetup";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./installStamp";
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { BridgeContext } from "../context";
|
|
3
|
+
import { INSTALL_STAMP_FILE, parseStamp, readStamp, writeStamp } from "./installStamp";
|
|
4
|
+
|
|
5
|
+
interface TestStamp {
|
|
6
|
+
buildId: string | null;
|
|
7
|
+
rconPassword?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const testContext = (seed: Record<string, string> = {}) => {
|
|
11
|
+
const stored = new Map(Object.entries(seed));
|
|
12
|
+
|
|
13
|
+
const context = {
|
|
14
|
+
files: {
|
|
15
|
+
async exists(path: string) {
|
|
16
|
+
return stored.has(path);
|
|
17
|
+
},
|
|
18
|
+
async read(path: string) {
|
|
19
|
+
return stored.get(path) ?? "";
|
|
20
|
+
},
|
|
21
|
+
async write(path: string, content: string) {
|
|
22
|
+
stored.set(path, content);
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
} as unknown as BridgeContext;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
context,
|
|
29
|
+
stored,
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe("parseStamp", () => {
|
|
34
|
+
test("reads a json object", () => {
|
|
35
|
+
expect(
|
|
36
|
+
parseStamp<TestStamp>(
|
|
37
|
+
JSON.stringify({
|
|
38
|
+
buildId: "123",
|
|
39
|
+
}),
|
|
40
|
+
),
|
|
41
|
+
).toEqual({
|
|
42
|
+
buildId: "123",
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("returns null for anything that is not a json object", () => {
|
|
47
|
+
expect(parseStamp("")).toBeNull();
|
|
48
|
+
expect(parseStamp("{")).toBeNull();
|
|
49
|
+
expect(parseStamp("null")).toBeNull();
|
|
50
|
+
expect(parseStamp("[1,2]")).toBeNull();
|
|
51
|
+
expect(parseStamp('"text"')).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("readStamp", () => {
|
|
56
|
+
test("returns null when the stamp file is absent", async () => {
|
|
57
|
+
const { context } = testContext();
|
|
58
|
+
|
|
59
|
+
expect(await readStamp(context)).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("reads the stamp the writer left behind", async () => {
|
|
63
|
+
const { context } = testContext();
|
|
64
|
+
|
|
65
|
+
await writeStamp<TestStamp>(context, {
|
|
66
|
+
buildId: "4711",
|
|
67
|
+
rconPassword: "secret",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(await readStamp<TestStamp>(context)).toEqual({
|
|
71
|
+
buildId: "4711",
|
|
72
|
+
rconPassword: "secret",
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("writes pretty json ending in a newline", async () => {
|
|
77
|
+
const { context, stored } = testContext();
|
|
78
|
+
|
|
79
|
+
await writeStamp(context, {
|
|
80
|
+
buildId: "1",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(stored.get(INSTALL_STAMP_FILE)).toBe('{\n "buildId": "1"\n}\n');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("honours a custom stamp path", async () => {
|
|
87
|
+
const { context, stored } = testContext();
|
|
88
|
+
|
|
89
|
+
await writeStamp(
|
|
90
|
+
context,
|
|
91
|
+
{
|
|
92
|
+
ready: true,
|
|
93
|
+
},
|
|
94
|
+
".serverk-wine.json",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(stored.has(INSTALL_STAMP_FILE)).toBe(false);
|
|
98
|
+
expect(await readStamp(context, ".serverk-wine.json")).toEqual({
|
|
99
|
+
ready: true,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("returns null when the read fails after the file was seen", async () => {
|
|
104
|
+
const context = {
|
|
105
|
+
files: {
|
|
106
|
+
async exists() {
|
|
107
|
+
return true;
|
|
108
|
+
},
|
|
109
|
+
async read() {
|
|
110
|
+
throw new Error("vanished");
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
} as unknown as BridgeContext;
|
|
114
|
+
|
|
115
|
+
expect(await readStamp(context)).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("returns null when the stamp file is corrupt", async () => {
|
|
119
|
+
const { context } = testContext({
|
|
120
|
+
[INSTALL_STAMP_FILE]: "not json",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(await readStamp(context)).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { BridgeContext } from "../context";
|
|
2
|
+
|
|
3
|
+
export const INSTALL_STAMP_FILE = ".serverk-install.json";
|
|
4
|
+
|
|
5
|
+
export const parseStamp = <Stamp extends object = Record<string, unknown>>(raw: string): Stamp | null => {
|
|
6
|
+
try {
|
|
7
|
+
const parsed: unknown = JSON.parse(raw);
|
|
8
|
+
|
|
9
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return parsed as Stamp;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const readStamp = async <Stamp extends object = Record<string, unknown>>(
|
|
20
|
+
context: BridgeContext,
|
|
21
|
+
path: string = INSTALL_STAMP_FILE,
|
|
22
|
+
): Promise<Stamp | null> => {
|
|
23
|
+
if (!(await context.files.exists(path))) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
return parseStamp<Stamp>(await context.files.read(path));
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const writeStamp = async <Stamp extends object>(
|
|
35
|
+
context: BridgeContext,
|
|
36
|
+
stamp: Stamp,
|
|
37
|
+
path: string = INSTALL_STAMP_FILE,
|
|
38
|
+
): Promise<void> => {
|
|
39
|
+
await context.files.write(path, `${JSON.stringify(stamp, null, 2)}\n`);
|
|
40
|
+
};
|
package/src/main.ts
CHANGED