@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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { isSensitiveKey, keySegments } from "./sensitiveKey";
|
|
3
|
+
|
|
4
|
+
describe("isSensitiveKey", () => {
|
|
5
|
+
test("reads a key as whole segments, split on underscores, case changes and digits", () => {
|
|
6
|
+
expect(keySegments("DISCORD_BOT_TOKEN")).toEqual([
|
|
7
|
+
"DISCORD",
|
|
8
|
+
"BOT",
|
|
9
|
+
"TOKEN",
|
|
10
|
+
]);
|
|
11
|
+
expect(keySegments("discordToken")).toEqual([
|
|
12
|
+
"DISCORD",
|
|
13
|
+
"TOKEN",
|
|
14
|
+
]);
|
|
15
|
+
expect(keySegments("TOKEN2__X")).toEqual([
|
|
16
|
+
"TOKEN",
|
|
17
|
+
"2",
|
|
18
|
+
"X",
|
|
19
|
+
]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("hides every key that names a secret in one of its segments", () => {
|
|
23
|
+
for (const key of [
|
|
24
|
+
"DISCORD_TOKEN",
|
|
25
|
+
"TELEGRAM_BOT_TOKEN",
|
|
26
|
+
"discordToken",
|
|
27
|
+
"TOKEN2",
|
|
28
|
+
"API_KEY",
|
|
29
|
+
"OPENAI_APIKEY",
|
|
30
|
+
"CLIENT_SECRET",
|
|
31
|
+
"DB_PASSWORD",
|
|
32
|
+
"SMTP_PASS",
|
|
33
|
+
"AUTH_HEADER",
|
|
34
|
+
"GOOGLE_CREDENTIALS",
|
|
35
|
+
"GCP_CREDENTIAL",
|
|
36
|
+
"PRIVATE_KEY_PEM",
|
|
37
|
+
"DISCORD_WEBHOOK",
|
|
38
|
+
"SENTRY_DSN",
|
|
39
|
+
"JWT_SIGNING_KEY",
|
|
40
|
+
"PASSWORD_SALT",
|
|
41
|
+
"SESSION_SECRET",
|
|
42
|
+
"COOKIE_NAME",
|
|
43
|
+
"SIGNING_ALGORITHM",
|
|
44
|
+
]) {
|
|
45
|
+
expect(isSensitiveKey(key), key).toBe(true);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("hides connection strings, which carry their password inside the link", () => {
|
|
50
|
+
for (const key of [
|
|
51
|
+
"DATABASE_URL",
|
|
52
|
+
"DB_URI",
|
|
53
|
+
"MONGODB_URI",
|
|
54
|
+
"MONGO_URL",
|
|
55
|
+
"POSTGRES_URL",
|
|
56
|
+
"MYSQL_URL",
|
|
57
|
+
"REDIS_URL",
|
|
58
|
+
"REDISURL",
|
|
59
|
+
"AMQP_URL",
|
|
60
|
+
"CONNECTION_STRING",
|
|
61
|
+
"SHADOW_DATABASE_URL",
|
|
62
|
+
"DATABASE_URL_POOLED",
|
|
63
|
+
]) {
|
|
64
|
+
expect(isSensitiveKey(key), key).toBe(true);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("leaves plain settings visible, including words that only contain a secret word", () => {
|
|
69
|
+
for (const key of [
|
|
70
|
+
"AUTHOR",
|
|
71
|
+
"AUTHOR_NAME",
|
|
72
|
+
"KEYWORD",
|
|
73
|
+
"KEYWORDS",
|
|
74
|
+
"MONKEY_MODE",
|
|
75
|
+
"BYPASS_CACHE",
|
|
76
|
+
"COMPASS_ID",
|
|
77
|
+
"TOKENIZER",
|
|
78
|
+
"URL",
|
|
79
|
+
"API_URL",
|
|
80
|
+
"BASE_URI",
|
|
81
|
+
"WEBSITE_URL",
|
|
82
|
+
"GUILD_ID",
|
|
83
|
+
"LOG_LEVEL",
|
|
84
|
+
"PREFIX",
|
|
85
|
+
"DATABASE_NAME",
|
|
86
|
+
"REDIS_HOST",
|
|
87
|
+
]) {
|
|
88
|
+
expect(isSensitiveKey(key), key).toBe(false);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const SENSITIVE_SEGMENTS: ReadonlySet<string> = new Set([
|
|
2
|
+
"TOKEN",
|
|
3
|
+
"TOKENS",
|
|
4
|
+
"SECRET",
|
|
5
|
+
"SECRETS",
|
|
6
|
+
"PASSWORD",
|
|
7
|
+
"PASSWD",
|
|
8
|
+
"PASSPHRASE",
|
|
9
|
+
"PASS",
|
|
10
|
+
"KEY",
|
|
11
|
+
"KEYS",
|
|
12
|
+
"APIKEY",
|
|
13
|
+
"AUTH",
|
|
14
|
+
"CREDENTIAL",
|
|
15
|
+
"CREDENTIALS",
|
|
16
|
+
"PRIVATE",
|
|
17
|
+
"WEBHOOK",
|
|
18
|
+
"DSN",
|
|
19
|
+
"JWT",
|
|
20
|
+
"SALT",
|
|
21
|
+
"COOKIE",
|
|
22
|
+
"SESSION",
|
|
23
|
+
"SIGNING",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
export const CONNECTION_STRING_PATTERN =
|
|
27
|
+
/(?:^|_)(?:DATABASE|DB|MONGO|MONGODB|POSTGRES|POSTGRESQL|MYSQL|REDIS|AMQP|CONNECTION)_?(?:URL|URI|STRING)(?:_|$)/;
|
|
28
|
+
|
|
29
|
+
export const keySegments = (key: string): string[] => {
|
|
30
|
+
return key
|
|
31
|
+
.replace(/([a-z])(?=[A-Z])/g, "$1_")
|
|
32
|
+
.replace(/([A-Za-z])(?=[0-9])|([0-9])(?=[A-Za-z])/g, "$1$2_")
|
|
33
|
+
.toUpperCase()
|
|
34
|
+
.split("_")
|
|
35
|
+
.filter((segment) => segment.length > 0);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const isSensitiveKey = (key: string) => {
|
|
39
|
+
const segments = keySegments(key);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
segments.some((segment) => SENSITIVE_SEGMENTS.has(segment)) || CONNECTION_STRING_PATTERN.test(segments.join("_"))
|
|
43
|
+
);
|
|
44
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { BotFramework } from "./botFramework";
|
|
3
|
+
import { BotPlatform } from "./botPlatform";
|
|
4
|
+
import { BotRuntime } from "./botRuntime";
|
|
5
|
+
import { parseServerkJson } from "./serverkJson";
|
|
6
|
+
|
|
7
|
+
describe("parseServerkJson", () => {
|
|
8
|
+
test("reads every field and ignores unknown ones", () => {
|
|
9
|
+
expect(
|
|
10
|
+
parseServerkJson(
|
|
11
|
+
JSON.stringify({
|
|
12
|
+
$schema: "https://serverk.gg/serverk.json",
|
|
13
|
+
runtime: "python",
|
|
14
|
+
platform: "discord",
|
|
15
|
+
framework: "discord-py",
|
|
16
|
+
start: " python -m bot ",
|
|
17
|
+
install: "pip install -r requirements/prod.txt",
|
|
18
|
+
env: [
|
|
19
|
+
"GUILD_ID",
|
|
20
|
+
{
|
|
21
|
+
key: "DISCORD_TOKEN",
|
|
22
|
+
sensitive: true,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
}),
|
|
26
|
+
),
|
|
27
|
+
).toEqual({
|
|
28
|
+
runtime: BotRuntime.Python,
|
|
29
|
+
platform: BotPlatform.Discord,
|
|
30
|
+
framework: BotFramework.DiscordPy,
|
|
31
|
+
start: "python -m bot",
|
|
32
|
+
install: "pip install -r requirements/prod.txt",
|
|
33
|
+
env: [
|
|
34
|
+
{
|
|
35
|
+
key: "GUILD_ID",
|
|
36
|
+
sensitive: null,
|
|
37
|
+
example: null,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
key: "DISCORD_TOKEN",
|
|
41
|
+
sensitive: true,
|
|
42
|
+
example: null,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("accepts an empty object", () => {
|
|
49
|
+
expect(parseServerkJson("{}")).toEqual({
|
|
50
|
+
runtime: null,
|
|
51
|
+
platform: null,
|
|
52
|
+
framework: null,
|
|
53
|
+
start: null,
|
|
54
|
+
install: null,
|
|
55
|
+
env: [],
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("refuses a file with any field it cannot trust", () => {
|
|
60
|
+
for (const text of [
|
|
61
|
+
"[]",
|
|
62
|
+
"not json",
|
|
63
|
+
'{"runtime":"go"}',
|
|
64
|
+
'{"platform":"slack"}',
|
|
65
|
+
'{"start":""}',
|
|
66
|
+
'{"start":"a\\nb"}',
|
|
67
|
+
'{"install":""}',
|
|
68
|
+
'{"install":["npm","ci"]}',
|
|
69
|
+
`{"start":"${"x".repeat(1025)}"}`,
|
|
70
|
+
'{"env":["1BAD"]}',
|
|
71
|
+
'{"env":[{"key":"A","sensitive":"yes"}]}',
|
|
72
|
+
]) {
|
|
73
|
+
expect(parseServerkJson(text)).toBeNull();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("counts a command's length in characters, not UTF-16 units", () => {
|
|
78
|
+
const command = `echo ${"🤖".repeat(1019)}`;
|
|
79
|
+
|
|
80
|
+
expect(
|
|
81
|
+
parseServerkJson(
|
|
82
|
+
JSON.stringify({
|
|
83
|
+
start: command,
|
|
84
|
+
}),
|
|
85
|
+
)?.start,
|
|
86
|
+
).toBe(command);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { type BotFramework, isBotFramework } from "./botFramework";
|
|
2
|
+
import { type BotPlatform, isBotPlatform } from "./botPlatform";
|
|
3
|
+
import { type BotRuntime, isBotRuntime } from "./botRuntime";
|
|
4
|
+
import { isEnvKey } from "./envKey";
|
|
5
|
+
|
|
6
|
+
export const SERVERK_JSON = "serverk.json";
|
|
7
|
+
|
|
8
|
+
export interface ServerkJsonEnv {
|
|
9
|
+
key: string;
|
|
10
|
+
sensitive: boolean | null;
|
|
11
|
+
example: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ServerkJson {
|
|
15
|
+
runtime: BotRuntime | null;
|
|
16
|
+
platform: BotPlatform | null;
|
|
17
|
+
framework: BotFramework | null;
|
|
18
|
+
start: string | null;
|
|
19
|
+
install: string | null;
|
|
20
|
+
env: ServerkJsonEnv[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const SERVERK_JSON_COMMAND_LENGTH_LIMIT = 1024;
|
|
24
|
+
|
|
25
|
+
export const SERVERK_JSON_EXAMPLE_LENGTH_LIMIT = 256;
|
|
26
|
+
|
|
27
|
+
export const SERVERK_JSON_ENV_LIMIT = 100;
|
|
28
|
+
|
|
29
|
+
const codePointLength = (value: string) => {
|
|
30
|
+
return Array.from(value).length;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type Parsed<Value> =
|
|
34
|
+
| {
|
|
35
|
+
ok: true;
|
|
36
|
+
value: Value;
|
|
37
|
+
}
|
|
38
|
+
| {
|
|
39
|
+
ok: false;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const accept = <Value>(value: Value): Parsed<Value> => ({
|
|
43
|
+
ok: true,
|
|
44
|
+
value,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const REFUSED = {
|
|
48
|
+
ok: false,
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
51
|
+
const optionalOf = <Value>(raw: unknown, guard: (value: string) => value is Value & string): Parsed<Value | null> => {
|
|
52
|
+
if (raw === undefined || raw === null) {
|
|
53
|
+
return accept(null);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return typeof raw === "string" && guard(raw) ? accept(raw) : REFUSED;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const commandOf = (raw: unknown): Parsed<string | null> => {
|
|
60
|
+
if (raw === undefined || raw === null) {
|
|
61
|
+
return accept(null);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (typeof raw !== "string") {
|
|
65
|
+
return REFUSED;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const command = raw.trim();
|
|
69
|
+
|
|
70
|
+
return command.length > 0 && codePointLength(command) <= SERVERK_JSON_COMMAND_LENGTH_LIMIT && !command.includes("\n")
|
|
71
|
+
? accept(command)
|
|
72
|
+
: REFUSED;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const envEntryOf = (raw: unknown): Parsed<ServerkJsonEnv> => {
|
|
76
|
+
if (typeof raw === "string") {
|
|
77
|
+
return isEnvKey(raw)
|
|
78
|
+
? accept({
|
|
79
|
+
key: raw,
|
|
80
|
+
sensitive: null,
|
|
81
|
+
example: null,
|
|
82
|
+
})
|
|
83
|
+
: REFUSED;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
87
|
+
return REFUSED;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const entry = raw as Record<string, unknown>;
|
|
91
|
+
const key = entry.key;
|
|
92
|
+
const sensitive = entry.sensitive ?? null;
|
|
93
|
+
const example = entry.example ?? null;
|
|
94
|
+
|
|
95
|
+
if (typeof key !== "string" || !isEnvKey(key)) {
|
|
96
|
+
return REFUSED;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (sensitive !== null && typeof sensitive !== "boolean") {
|
|
100
|
+
return REFUSED;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (
|
|
104
|
+
example !== null
|
|
105
|
+
&& (typeof example !== "string" || codePointLength(example) > SERVERK_JSON_EXAMPLE_LENGTH_LIMIT)
|
|
106
|
+
) {
|
|
107
|
+
return REFUSED;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return accept({
|
|
111
|
+
key,
|
|
112
|
+
sensitive,
|
|
113
|
+
example,
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const envOf = (raw: unknown): Parsed<ServerkJsonEnv[]> => {
|
|
118
|
+
if (raw === undefined || raw === null) {
|
|
119
|
+
return accept([]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!Array.isArray(raw) || raw.length > SERVERK_JSON_ENV_LIMIT) {
|
|
123
|
+
return REFUSED;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const entries: ServerkJsonEnv[] = [];
|
|
127
|
+
|
|
128
|
+
for (const item of raw) {
|
|
129
|
+
const entry = envEntryOf(item);
|
|
130
|
+
|
|
131
|
+
if (!entry.ok) {
|
|
132
|
+
return REFUSED;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
entries.push(entry.value);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return accept(entries);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const parseServerkJson = (text: string): ServerkJson | null => {
|
|
142
|
+
let raw: unknown;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
raw = JSON.parse(text);
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const document = raw as Record<string, unknown>;
|
|
155
|
+
const runtime = optionalOf(document.runtime, isBotRuntime);
|
|
156
|
+
const platform = optionalOf(document.platform, isBotPlatform);
|
|
157
|
+
const framework = optionalOf(document.framework, isBotFramework);
|
|
158
|
+
const start = commandOf(document.start);
|
|
159
|
+
const install = commandOf(document.install);
|
|
160
|
+
const env = envOf(document.env);
|
|
161
|
+
|
|
162
|
+
if (!runtime.ok || !platform.ok || !framework.ok || !start.ok || !install.ok || !env.ok) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
runtime: runtime.value,
|
|
168
|
+
platform: platform.value,
|
|
169
|
+
framework: framework.value,
|
|
170
|
+
start: start.value,
|
|
171
|
+
install: install.value,
|
|
172
|
+
env: env.value,
|
|
173
|
+
};
|
|
174
|
+
};
|
package/src/bot/index.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { renderDetectTable } from "../bot/detect";
|
|
6
|
+
import { runDetectTable } from "./detectTable";
|
|
7
|
+
|
|
8
|
+
describe("serverk-bridge detect-table", () => {
|
|
9
|
+
let root = "";
|
|
10
|
+
let restore = () => {};
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
root = mkdtempSync(join(tmpdir(), "serverk-detect-table-"));
|
|
14
|
+
|
|
15
|
+
const spy = spyOn(console, "log").mockImplementation(() => {});
|
|
16
|
+
|
|
17
|
+
restore = () => spy.mockRestore();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
restore();
|
|
22
|
+
rmSync(root, {
|
|
23
|
+
force: true,
|
|
24
|
+
recursive: true,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("writes the table the launchers read, then passes its own check", async () => {
|
|
29
|
+
const out = join(root, "detect.json");
|
|
30
|
+
|
|
31
|
+
await runDetectTable([
|
|
32
|
+
"--out",
|
|
33
|
+
out,
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
expect(readFileSync(out, "utf8")).toBe(renderDetectTable());
|
|
37
|
+
|
|
38
|
+
await runDetectTable([
|
|
39
|
+
"--out",
|
|
40
|
+
out,
|
|
41
|
+
"--check",
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("fails the check on a stale or missing table", async () => {
|
|
46
|
+
const out = join(root, "detect.json");
|
|
47
|
+
|
|
48
|
+
await expect(
|
|
49
|
+
runDetectTable([
|
|
50
|
+
"--out",
|
|
51
|
+
out,
|
|
52
|
+
"--check",
|
|
53
|
+
]),
|
|
54
|
+
).rejects.toThrow("is missing");
|
|
55
|
+
|
|
56
|
+
writeFileSync(out, "{}\n");
|
|
57
|
+
|
|
58
|
+
await expect(
|
|
59
|
+
runDetectTable([
|
|
60
|
+
"--out",
|
|
61
|
+
out,
|
|
62
|
+
"--check",
|
|
63
|
+
]),
|
|
64
|
+
).rejects.toThrow("is out of date");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { DETECT_TABLE_FILE, renderDetectTable } from "../bot/detect";
|
|
3
|
+
import { writeRenderedFile } from "./renderedFile";
|
|
4
|
+
|
|
5
|
+
const HELP = `serverk-bridge detect-table [options]
|
|
6
|
+
|
|
7
|
+
options:
|
|
8
|
+
--out <path> file to write (default: ${DETECT_TABLE_FILE})
|
|
9
|
+
--check fail when the file on disk is out of date instead of writing
|
|
10
|
+
--help show this help`;
|
|
11
|
+
|
|
12
|
+
export const runDetectTable = async (args: string[]) => {
|
|
13
|
+
const { values } = parseArgs({
|
|
14
|
+
args,
|
|
15
|
+
options: {
|
|
16
|
+
out: {
|
|
17
|
+
type: "string",
|
|
18
|
+
default: DETECT_TABLE_FILE,
|
|
19
|
+
},
|
|
20
|
+
check: {
|
|
21
|
+
type: "boolean",
|
|
22
|
+
default: false,
|
|
23
|
+
},
|
|
24
|
+
help: {
|
|
25
|
+
type: "boolean",
|
|
26
|
+
default: false,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (values.help) {
|
|
32
|
+
console.log(HELP);
|
|
33
|
+
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
writeRenderedFile({
|
|
38
|
+
out: values.out,
|
|
39
|
+
check: values.check,
|
|
40
|
+
rendered: renderDetectTable(),
|
|
41
|
+
command: "serverk-bridge detect-table",
|
|
42
|
+
});
|
|
43
|
+
};
|
package/src/cli/main.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { errorText } from "../runtime/bridgeLog";
|
|
4
4
|
import { runCompile } from "./compile";
|
|
5
|
+
import { runDetectTable } from "./detectTable";
|
|
5
6
|
import { runLink } from "./link";
|
|
6
7
|
import { runSchema } from "./schema";
|
|
7
8
|
import { runValidate } from "./validate";
|
|
@@ -9,10 +10,11 @@ import { runValidate } from "./validate";
|
|
|
9
10
|
const USAGE = `serverk-bridge <command>
|
|
10
11
|
|
|
11
12
|
commands:
|
|
12
|
-
validate
|
|
13
|
-
schema
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
validate validate serverk.yml, assets, and driver wiring
|
|
14
|
+
schema write the serverk.yml JSON Schema for editor tooling
|
|
15
|
+
detect-table write the bot detection table the launchers read
|
|
16
|
+
compile compile the bridge binary for amd64
|
|
17
|
+
link use a local bridge checkout while developing
|
|
16
18
|
|
|
17
19
|
run "serverk-bridge <command> --help" for options`;
|
|
18
20
|
|
|
@@ -32,6 +34,12 @@ try {
|
|
|
32
34
|
break;
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
case "detect-table": {
|
|
38
|
+
await runDetectTable(args);
|
|
39
|
+
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
case "compile": {
|
|
36
44
|
await runCompile(args);
|
|
37
45
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface RenderedFile {
|
|
5
|
+
out: string;
|
|
6
|
+
check: boolean;
|
|
7
|
+
rendered: string;
|
|
8
|
+
command: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const writeRenderedFile = (file: RenderedFile) => {
|
|
12
|
+
const target = resolve(file.out);
|
|
13
|
+
|
|
14
|
+
if (file.check) {
|
|
15
|
+
let current: string;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
current = readFileSync(target, "utf8");
|
|
19
|
+
} catch {
|
|
20
|
+
throw new Error(`${file.out} is missing — run "${file.command}" and commit it`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (current !== file.rendered) {
|
|
24
|
+
throw new Error(`${file.out} is out of date — run "${file.command}" and commit it`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(`${file.out} is up to date`);
|
|
28
|
+
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
writeFileSync(target, file.rendered);
|
|
33
|
+
|
|
34
|
+
console.log(`wrote ${file.out}`);
|
|
35
|
+
};
|
package/src/cli/schema.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
1
|
import { parseArgs } from "node:util";
|
|
4
2
|
import { renderJsonSchema, SCHEMA_FILE } from "../manifest";
|
|
3
|
+
import { writeRenderedFile } from "./renderedFile";
|
|
5
4
|
|
|
6
5
|
const HELP = `serverk-bridge schema [options]
|
|
7
6
|
|
|
@@ -35,28 +34,10 @@ export const runSchema = async (args: string[]) => {
|
|
|
35
34
|
return;
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
current = readFileSync(target, "utf8");
|
|
46
|
-
} catch {
|
|
47
|
-
throw new Error(`${values.out} is missing — run "serverk-bridge schema" and commit it`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (current !== rendered) {
|
|
51
|
-
throw new Error(`${values.out} is out of date — run "serverk-bridge schema" and commit it`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
console.log(`${values.out} is up to date`);
|
|
55
|
-
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
writeFileSync(target, rendered);
|
|
60
|
-
|
|
61
|
-
console.log(`wrote ${values.out}`);
|
|
37
|
+
writeRenderedFile({
|
|
38
|
+
out: values.out,
|
|
39
|
+
check: values.check,
|
|
40
|
+
rendered: renderJsonSchema(),
|
|
41
|
+
command: "serverk-bridge schema",
|
|
42
|
+
});
|
|
62
43
|
};
|