@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.
Files changed (119) hide show
  1. package/package.json +3 -1
  2. package/src/bot/botDeploy.test.ts +363 -0
  3. package/src/bot/botDeploy.ts +136 -0
  4. package/src/bot/botEnvironment.test.ts +189 -0
  5. package/src/bot/botEnvironment.ts +141 -0
  6. package/src/bot/botIdentity.test.ts +251 -0
  7. package/src/bot/botIdentity.ts +140 -0
  8. package/src/bot/botPanel.test.ts +246 -0
  9. package/src/bot/botPanel.ts +129 -0
  10. package/src/bot/botState.test.ts +45 -0
  11. package/src/bot/botState.ts +54 -0
  12. package/src/bot/botTerminal.test.ts +79 -0
  13. package/src/bot/botTerminal.ts +62 -0
  14. package/src/bot/botVariables.test.ts +193 -0
  15. package/src/bot/botVariables.ts +73 -0
  16. package/src/bot/detect/botFramework.ts +23 -0
  17. package/src/bot/detect/botFrameworks.ts +198 -0
  18. package/src/bot/detect/botInspectWarning.ts +15 -0
  19. package/src/bot/detect/botPackageManager.ts +6 -0
  20. package/src/bot/detect/botPlatform.ts +10 -0
  21. package/src/bot/detect/botRuntime.ts +15 -0
  22. package/src/bot/detect/botToken.test.ts +71 -0
  23. package/src/bot/detect/botToken.ts +126 -0
  24. package/src/bot/detect/detect.test.ts +543 -0
  25. package/src/bot/detect/detect.ts +842 -0
  26. package/src/bot/detect/detectResult.ts +40 -0
  27. package/src/bot/detect/detectTable.test.ts +72 -0
  28. package/src/bot/detect/detectTable.ts +166 -0
  29. package/src/bot/detect/envExample.test.ts +29 -0
  30. package/src/bot/detect/envExample.ts +48 -0
  31. package/src/bot/detect/envKey.ts +7 -0
  32. package/src/bot/detect/envScan.test.ts +39 -0
  33. package/src/bot/detect/envScan.ts +87 -0
  34. package/src/bot/detect/index.ts +17 -0
  35. package/src/bot/detect/inferBotPlatform.test.ts +60 -0
  36. package/src/bot/detect/inferBotPlatform.ts +28 -0
  37. package/src/bot/detect/keys.test.ts +28 -0
  38. package/src/bot/detect/keys.ts +64 -0
  39. package/src/bot/detect/sensitiveKey.test.ts +91 -0
  40. package/src/bot/detect/sensitiveKey.ts +44 -0
  41. package/src/bot/detect/serverkJson.test.ts +88 -0
  42. package/src/bot/detect/serverkJson.ts +174 -0
  43. package/src/bot/index.ts +8 -0
  44. package/src/cli/detectTable.test.ts +66 -0
  45. package/src/cli/detectTable.ts +43 -0
  46. package/src/cli/main.ts +12 -4
  47. package/src/cli/renderedFile.ts +35 -0
  48. package/src/cli/schema.ts +7 -26
  49. package/src/cli/validate.test.ts +283 -3
  50. package/src/cli/validate.ts +105 -7
  51. package/src/context/bridgeContext.ts +17 -1
  52. package/src/core/bridgeAction.ts +2 -0
  53. package/src/core/bridgeActivity.ts +9 -0
  54. package/src/core/bridgeAvailability.ts +10 -0
  55. package/src/core/bridgePort.test.ts +45 -0
  56. package/src/core/bridgePort.ts +26 -0
  57. package/src/core/bridgeUserError.test.ts +10 -0
  58. package/src/core/bridgeUserError.ts +6 -2
  59. package/src/core/describeDownloadFailure.test.ts +58 -0
  60. package/src/core/describeDownloadFailure.ts +69 -0
  61. package/src/core/index.ts +3 -0
  62. package/src/guides/compile.ts +2 -2
  63. package/src/manifest/index.ts +1 -0
  64. package/src/manifest/kind.ts +5 -0
  65. package/src/manifest/manifest.test.ts +630 -3
  66. package/src/manifest/manifest.ts +391 -87
  67. package/src/manifest/runtime.ts +2 -2
  68. package/src/manifest/source.ts +4 -4
  69. package/src/modules/bridge.ts +8 -1
  70. package/src/modules/bridgeLifecycleSubject.ts +8 -0
  71. package/src/modules/bridgeModule.ts +19 -1
  72. package/src/modules/bridgeStopSignal.ts +13 -0
  73. package/src/modules/index.ts +2 -0
  74. package/src/panel/bridgeDetail.ts +4 -1
  75. package/src/protocol/bridgeActivity.ts +54 -0
  76. package/src/protocol/bridgeConfig.ts +2 -0
  77. package/src/protocol/bridgeEvents.ts +30 -0
  78. package/src/protocol/bridgeFrame.ts +10 -1
  79. package/src/protocol/index.ts +1 -0
  80. package/src/runtime/bridgeActions.ts +19 -2
  81. package/src/runtime/bridgeActivity.test.ts +318 -0
  82. package/src/runtime/bridgeActivity.ts +268 -0
  83. package/src/runtime/bridgeConfigReader.test.ts +80 -0
  84. package/src/runtime/bridgeConfigReader.ts +20 -0
  85. package/src/runtime/bridgeContext.test.ts +247 -0
  86. package/src/runtime/bridgeContext.ts +43 -10
  87. package/src/runtime/bridgeDriver.test.ts +153 -1
  88. package/src/runtime/bridgeDriver.ts +64 -8
  89. package/src/runtime/bridgeFiles.test.ts +62 -0
  90. package/src/runtime/bridgeFiles.ts +12 -2
  91. package/src/runtime/bridgeLiveConfig.ts +18 -0
  92. package/src/runtime/bridgeLog.ts +10 -0
  93. package/src/runtime/bridgeRedaction.test.ts +410 -0
  94. package/src/runtime/bridgeRedaction.ts +214 -0
  95. package/src/runtime/bridgeRefusalError.ts +13 -0
  96. package/src/runtime/bridgeRegistry.test.ts +1056 -10
  97. package/src/runtime/bridgeRegistry.ts +464 -50
  98. package/src/runtime/bridgeScopedContext.ts +55 -0
  99. package/src/runtime/bridgeServer.test.ts +534 -4
  100. package/src/runtime/bridgeServer.ts +145 -38
  101. package/src/runtime/bridgeSetupHandlers.test.ts +156 -5
  102. package/src/runtime/bridgeSetupHandlers.ts +59 -14
  103. package/src/runtime/bridgeSupervisor.test.ts +670 -2
  104. package/src/runtime/bridgeSupervisor.ts +219 -67
  105. package/src/runtime/serve.ts +87 -11
  106. package/src/steam/steamcmd.test.ts +32 -1
  107. package/src/steam/steamcmd.ts +27 -4
  108. package/src/utils/changedSettingKeys.test.ts +65 -0
  109. package/src/utils/changedSettingKeys.ts +6 -0
  110. package/src/utils/describeExecFailure.test.ts +34 -0
  111. package/src/utils/describeExecFailure.ts +9 -0
  112. package/src/utils/index.ts +3 -0
  113. package/src/utils/sameSettingValue.test.ts +35 -0
  114. package/src/utils/sameSettingValue.ts +40 -0
  115. package/src/wire/activity.test.ts +95 -0
  116. package/src/wire/activity.ts +41 -0
  117. package/src/wire/index.ts +1 -0
  118. package/src/wire/panel.test.ts +67 -1
  119. package/src/wire/panel.ts +11 -0
@@ -0,0 +1,40 @@
1
+ import type { BotFramework } from "./botFramework";
2
+ import type { BotInspectWarning } from "./botInspectWarning";
3
+ import type { BotPackageManager } from "./botPackageManager";
4
+ import type { BotPlatform } from "./botPlatform";
5
+ import type { BotRuntime } from "./botRuntime";
6
+
7
+ export interface DetectEntry {
8
+ path: string;
9
+ size?: number;
10
+ }
11
+
12
+ export interface DetectInput {
13
+ files: readonly DetectEntry[];
14
+ contents: Readonly<Record<string, string>>;
15
+ }
16
+
17
+ export interface DetectedCommand {
18
+ command: string;
19
+ source: string;
20
+ }
21
+
22
+ export interface DetectedEnvKey {
23
+ key: string;
24
+ sensitive: boolean;
25
+ source: string;
26
+ example: string | null;
27
+ }
28
+
29
+ export interface DetectResult {
30
+ runtime: BotRuntime | null;
31
+ package: string | null;
32
+ packageManager: BotPackageManager | null;
33
+ framework: BotFramework | null;
34
+ platform: BotPlatform | null;
35
+ install: DetectedCommand | null;
36
+ start: DetectedCommand | null;
37
+ env: DetectedEnvKey[];
38
+ tokenKey: string | null;
39
+ warnings: BotInspectWarning[];
40
+ }
@@ -0,0 +1,72 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { BotFramework } from "./botFramework";
3
+ import { DETECT_TABLE, renderDetectTable } from "./detectTable";
4
+ import { isEnvKey } from "./envKey";
5
+ import { parseServerkJson } from "./serverkJson";
6
+
7
+ describe("renderDetectTable", () => {
8
+ test("renders the table the launchers read as stable, tab-indented JSON", () => {
9
+ const rendered = renderDetectTable();
10
+
11
+ expect(rendered.endsWith("}\n")).toBe(true);
12
+ expect(rendered).toContain('\t"version": 1,');
13
+ expect(JSON.parse(rendered)).toEqual(DETECT_TABLE);
14
+ });
15
+
16
+ test("names an install command for every manager a lockfile or manifest points at", () => {
17
+ for (const lockfile of DETECT_TABLE.node.lockfiles) {
18
+ expect(DETECT_TABLE.node.installCommands[lockfile.manager]).toBeDefined();
19
+ }
20
+
21
+ expect(DETECT_TABLE.node.installCommands[DETECT_TABLE.node.unlockedManager]).toBeDefined();
22
+
23
+ for (const manifest of DETECT_TABLE.python.manifests) {
24
+ expect(DETECT_TABLE.python.installArguments[manifest.manager]).toBeDefined();
25
+ }
26
+ });
27
+
28
+ test("carries the serverk.json rules the launchers need to agree with the parser", () => {
29
+ const rules = DETECT_TABLE.serverkJson;
30
+ const envKey = new RegExp(rules.envKey);
31
+
32
+ expect(rules.file).toBe("serverk.json");
33
+ expect(rules.frameworks).toEqual(Object.values(BotFramework));
34
+ expect(envKey.test("DISCORD_TOKEN")).toBe(isEnvKey("DISCORD_TOKEN"));
35
+ expect(envKey.test("1BAD")).toBe(isEnvKey("1BAD"));
36
+ expect(
37
+ parseServerkJson(
38
+ JSON.stringify({
39
+ start: "x".repeat(rules.commandLengthLimit),
40
+ }),
41
+ ),
42
+ ).not.toBeNull();
43
+ expect(
44
+ parseServerkJson(
45
+ JSON.stringify({
46
+ start: "x".repeat(rules.commandLengthLimit + 1),
47
+ }),
48
+ ),
49
+ ).toBeNull();
50
+ expect(
51
+ parseServerkJson(
52
+ JSON.stringify({
53
+ env: Array.from(
54
+ {
55
+ length: rules.envLimit + 1,
56
+ },
57
+ (_, index) => `KEY_${index}`,
58
+ ),
59
+ }),
60
+ ),
61
+ ).toBeNull();
62
+ });
63
+
64
+ test("compiles the Procfile line in both JavaScript and Python syntax", () => {
65
+ const line = new RegExp(DETECT_TABLE.procfile.line);
66
+
67
+ expect(line.exec("worker: node bot.js")?.slice(1)).toEqual([
68
+ "worker",
69
+ "node bot.js",
70
+ ]);
71
+ });
72
+ });
@@ -0,0 +1,166 @@
1
+ import { BotFramework } from "./botFramework";
2
+ import { BotPlatform } from "./botPlatform";
3
+ import { BotRuntime } from "./botRuntime";
4
+ import { ENV_KEY_LENGTH_LIMIT, ENV_KEY_PATTERN } from "./envKey";
5
+ import {
6
+ SERVERK_JSON,
7
+ SERVERK_JSON_COMMAND_LENGTH_LIMIT,
8
+ SERVERK_JSON_ENV_LIMIT,
9
+ SERVERK_JSON_EXAMPLE_LENGTH_LIMIT,
10
+ } from "./serverkJson";
11
+
12
+ export const DETECT_TABLE_VERSION = 1;
13
+
14
+ export const DETECT_TABLE_FILE = "image/detect.json";
15
+
16
+ export interface DetectLockfile {
17
+ file: string;
18
+ manager: string;
19
+ }
20
+
21
+ export interface DetectProcfile {
22
+ file: string;
23
+ line: string;
24
+ processes: string[];
25
+ }
26
+
27
+ export interface ServerkJsonDetectTable {
28
+ file: string;
29
+ commandLengthLimit: number;
30
+ runtimes: string[];
31
+ platforms: string[];
32
+ frameworks: string[];
33
+ envLimit: number;
34
+ envKey: string;
35
+ envKeyLengthLimit: number;
36
+ exampleLengthLimit: number;
37
+ }
38
+
39
+ export interface NodeDetectTable {
40
+ manifest: string;
41
+ startFiles: string[];
42
+ lockfiles: DetectLockfile[];
43
+ installCommands: Record<string, string[]>;
44
+ unlockedManager: string;
45
+ }
46
+
47
+ export interface PythonDetectTable {
48
+ startFiles: string[];
49
+ manifests: DetectLockfile[];
50
+ installArguments: Record<string, string[]>;
51
+ }
52
+
53
+ export interface DetectTable {
54
+ version: number;
55
+ procfile: DetectProcfile;
56
+ serverkJson: ServerkJsonDetectTable;
57
+ node: NodeDetectTable;
58
+ python: PythonDetectTable;
59
+ }
60
+
61
+ export const DETECT_TABLE: DetectTable = {
62
+ version: DETECT_TABLE_VERSION,
63
+ procfile: {
64
+ file: "Procfile",
65
+ line: "^(worker|bot)\\s*:\\s*(.+)$",
66
+ processes: [
67
+ "worker",
68
+ "bot",
69
+ ],
70
+ },
71
+ serverkJson: {
72
+ file: SERVERK_JSON,
73
+ commandLengthLimit: SERVERK_JSON_COMMAND_LENGTH_LIMIT,
74
+ runtimes: Object.values(BotRuntime),
75
+ platforms: Object.values(BotPlatform),
76
+ frameworks: Object.values(BotFramework),
77
+ envLimit: SERVERK_JSON_ENV_LIMIT,
78
+ envKey: ENV_KEY_PATTERN.source,
79
+ envKeyLengthLimit: ENV_KEY_LENGTH_LIMIT,
80
+ exampleLengthLimit: SERVERK_JSON_EXAMPLE_LENGTH_LIMIT,
81
+ },
82
+ node: {
83
+ manifest: "package.json",
84
+ startFiles: [
85
+ "index.js",
86
+ "src/index.js",
87
+ "bot.js",
88
+ "src/bot.js",
89
+ "index.ts",
90
+ ],
91
+ lockfiles: [
92
+ {
93
+ file: "package-lock.json",
94
+ manager: "npm",
95
+ },
96
+ {
97
+ file: "npm-shrinkwrap.json",
98
+ manager: "npm",
99
+ },
100
+ {
101
+ file: "pnpm-lock.yaml",
102
+ manager: "pnpm",
103
+ },
104
+ {
105
+ file: "yarn.lock",
106
+ manager: "yarn",
107
+ },
108
+ ],
109
+ installCommands: {
110
+ npm: [
111
+ "npm",
112
+ "ci",
113
+ ],
114
+ pnpm: [
115
+ "corepack",
116
+ "pnpm",
117
+ "install",
118
+ "--frozen-lockfile",
119
+ ],
120
+ yarn: [
121
+ "corepack",
122
+ "yarn",
123
+ "install",
124
+ ],
125
+ "npm-install": [
126
+ "npm",
127
+ "install",
128
+ ],
129
+ },
130
+ unlockedManager: "npm-install",
131
+ },
132
+ python: {
133
+ startFiles: [
134
+ "main.py",
135
+ "bot.py",
136
+ "app.py",
137
+ "src/main.py",
138
+ "src/bot.py",
139
+ ],
140
+ manifests: [
141
+ {
142
+ file: "requirements.txt",
143
+ manager: "requirements",
144
+ },
145
+ {
146
+ file: "pyproject.toml",
147
+ manager: "project",
148
+ },
149
+ ],
150
+ installArguments: {
151
+ requirements: [
152
+ "install",
153
+ "-r",
154
+ "requirements.txt",
155
+ ],
156
+ project: [
157
+ "install",
158
+ ".",
159
+ ],
160
+ },
161
+ },
162
+ };
163
+
164
+ export const renderDetectTable = (table: DetectTable = DETECT_TABLE) => {
165
+ return `${JSON.stringify(table, null, "\t")}\n`;
166
+ };
@@ -0,0 +1,29 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { parseEnvExample } from "./envExample";
3
+
4
+ describe("parseEnvExample", () => {
5
+ test("reads keys and example values the way dotenv files write them", () => {
6
+ expect(
7
+ parseEnvExample(
8
+ "# Discord\r\nDISCORD_TOKEN=\r\nexport PREFIX=\"!\" # command prefix\nOWNER_ID = 1234 # yours\nQUOTED='a # b'\nnot a line\n1BAD=x\n",
9
+ ),
10
+ ).toEqual([
11
+ {
12
+ key: "DISCORD_TOKEN",
13
+ value: "",
14
+ },
15
+ {
16
+ key: "PREFIX",
17
+ value: "!",
18
+ },
19
+ {
20
+ key: "OWNER_ID",
21
+ value: "1234",
22
+ },
23
+ {
24
+ key: "QUOTED",
25
+ value: "a # b",
26
+ },
27
+ ]);
28
+ });
29
+ });
@@ -0,0 +1,48 @@
1
+ import { isEnvKey } from "./envKey";
2
+
3
+ export const ENV_EXAMPLE_FILES: readonly string[] = [
4
+ ".env.example",
5
+ ".env.sample",
6
+ ".env.template",
7
+ ];
8
+
9
+ export interface EnvExampleEntry {
10
+ key: string;
11
+ value: string;
12
+ }
13
+
14
+ const ASSIGNMENT = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/;
15
+
16
+ const INLINE_COMMENT = /\s+#.*$/;
17
+
18
+ const unquote = (raw: string) => {
19
+ const quote = raw[0];
20
+
21
+ if (quote === '"' || quote === "'") {
22
+ const end = raw.indexOf(quote, 1);
23
+
24
+ return end === -1 ? raw.slice(1) : raw.slice(1, end);
25
+ }
26
+
27
+ return raw.replace(INLINE_COMMENT, "").trim();
28
+ };
29
+
30
+ export const parseEnvExample = (text: string): EnvExampleEntry[] => {
31
+ const entries: EnvExampleEntry[] = [];
32
+
33
+ for (const line of text.replace(/^/, "").split(/\r?\n/)) {
34
+ const match = ASSIGNMENT.exec(line);
35
+ const key = match?.[1];
36
+
37
+ if (!key || !isEnvKey(key)) {
38
+ continue;
39
+ }
40
+
41
+ entries.push({
42
+ key,
43
+ value: unquote((match[2] ?? "").trim()),
44
+ });
45
+ }
46
+
47
+ return entries;
48
+ };
@@ -0,0 +1,7 @@
1
+ export const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
2
+
3
+ export const ENV_KEY_LENGTH_LIMIT = 64;
4
+
5
+ export const isEnvKey = (key: string) => {
6
+ return key.length <= ENV_KEY_LENGTH_LIMIT && ENV_KEY_PATTERN.test(key);
7
+ };
@@ -0,0 +1,39 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { scanEnvKeys } from "./envScan";
3
+
4
+ describe("scanEnvKeys", () => {
5
+ test("finds every way Node code reads a variable", () => {
6
+ expect(
7
+ scanEnvKeys(
8
+ "src/index.ts",
9
+ 'process.env.A; process.env["B"]; process.env[`C`]; const { D, E: e, F = 1, ...rest } = process.env;',
10
+ ),
11
+ ).toEqual([
12
+ "A",
13
+ "B",
14
+ "C",
15
+ "D",
16
+ "E",
17
+ "F",
18
+ ]);
19
+ });
20
+
21
+ test("finds every way Python code reads a variable", () => {
22
+ expect(
23
+ scanEnvKeys(
24
+ "bot/main.py",
25
+ 'os.environ["A"]\nos.environ.get("B", "x")\nos.getenv(\'C\')\nfrom os import getenv, environ\ngetenv("D")\nenviron["E"]\nmyos.getenv("NO")',
26
+ ),
27
+ ).toEqual([
28
+ "A",
29
+ "B",
30
+ "C",
31
+ "D",
32
+ "E",
33
+ ]);
34
+ });
35
+
36
+ test("reads nothing from files that are not code", () => {
37
+ expect(scanEnvKeys("README.md", "process.env.A")).toEqual([]);
38
+ });
39
+ });
@@ -0,0 +1,87 @@
1
+ export const NODE_SOURCE_EXTENSIONS: readonly string[] = [
2
+ ".js",
3
+ ".mjs",
4
+ ".cjs",
5
+ ".ts",
6
+ ".mts",
7
+ ".cts",
8
+ ];
9
+
10
+ export const PYTHON_SOURCE_EXTENSIONS: readonly string[] = [
11
+ ".py",
12
+ ];
13
+
14
+ const NAME = "([A-Za-z_][A-Za-z0-9_]*)";
15
+
16
+ const NODE_PATTERNS: readonly RegExp[] = [
17
+ new RegExp(`\\bprocess\\.env\\.${NAME}`, "g"),
18
+ new RegExp(`\\bprocess\\.env\\[\\s*["'\`]${NAME}["'\`]\\s*\\]`, "g"),
19
+ ];
20
+
21
+ const NODE_DESTRUCTURE = /\{([^{}]{1,2000})\}\s*=\s*process\.env\b/g;
22
+
23
+ const DESTRUCTURED_NAME = /^\s*([A-Za-z_][A-Za-z0-9_]*)/;
24
+
25
+ const PYTHON_PATTERNS: readonly RegExp[] = [
26
+ new RegExp(`(?<![\\w.])(?:os\\.)?environ\\[\\s*["']${NAME}["']\\s*\\]`, "g"),
27
+ new RegExp(`(?<![\\w.])(?:os\\.)?environ\\.get\\(\\s*["']${NAME}["']`, "g"),
28
+ new RegExp(`(?<![\\w.])(?:os\\.)?getenv\\(\\s*["']${NAME}["']`, "g"),
29
+ ];
30
+
31
+ interface Found {
32
+ at: number;
33
+ name: string;
34
+ }
35
+
36
+ export const isNodeSource = (path: string) => {
37
+ return NODE_SOURCE_EXTENSIONS.some((extension) => path.endsWith(extension));
38
+ };
39
+
40
+ export const isPythonSource = (path: string) => {
41
+ return PYTHON_SOURCE_EXTENSIONS.some((extension) => path.endsWith(extension));
42
+ };
43
+
44
+ const collect = (text: string, patterns: readonly RegExp[], found: Found[]) => {
45
+ for (const pattern of patterns) {
46
+ for (const match of text.matchAll(pattern)) {
47
+ if (match[1]) {
48
+ found.push({
49
+ at: match.index,
50
+ name: match[1],
51
+ });
52
+ }
53
+ }
54
+ }
55
+ };
56
+
57
+ const destructured = (text: string, found: Found[]) => {
58
+ for (const match of text.matchAll(NODE_DESTRUCTURE)) {
59
+ for (const part of (match[1] ?? "").split(",")) {
60
+ const name = DESTRUCTURED_NAME.exec(part)?.[1];
61
+
62
+ if (name && !part.trimStart().startsWith("...")) {
63
+ found.push({
64
+ at: match.index,
65
+ name,
66
+ });
67
+ }
68
+ }
69
+ }
70
+ };
71
+
72
+ export const scanEnvKeys = (path: string, text: string): string[] => {
73
+ const found: Found[] = [];
74
+
75
+ if (isNodeSource(path)) {
76
+ collect(text, NODE_PATTERNS, found);
77
+ destructured(text, found);
78
+ }
79
+
80
+ if (isPythonSource(path)) {
81
+ collect(text, PYTHON_PATTERNS, found);
82
+ }
83
+
84
+ return [
85
+ ...new Set(found.sort((a, b) => a.at - b.at).map((entry) => entry.name)),
86
+ ];
87
+ };
@@ -0,0 +1,17 @@
1
+ export * from "./botFramework";
2
+ export * from "./botFrameworks";
3
+ export * from "./botInspectWarning";
4
+ export * from "./botPackageManager";
5
+ export * from "./botPlatform";
6
+ export * from "./botRuntime";
7
+ export * from "./botToken";
8
+ export * from "./detect";
9
+ export * from "./detectResult";
10
+ export * from "./detectTable";
11
+ export * from "./envExample";
12
+ export * from "./envKey";
13
+ export * from "./envScan";
14
+ export * from "./inferBotPlatform";
15
+ export * from "./keys";
16
+ export * from "./sensitiveKey";
17
+ export * from "./serverkJson";
@@ -0,0 +1,60 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { BotPlatform } from "./botPlatform";
3
+ import { inferBotPlatform } from "./inferBotPlatform";
4
+
5
+ const TELEGRAM_TOKEN = "7123456789:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw";
6
+
7
+ describe("inferBotPlatform", () => {
8
+ test("takes the explicit platform first", () => {
9
+ expect(
10
+ inferBotPlatform({
11
+ variables: {
12
+ BOT_PLATFORM: "telegram",
13
+ DISCORD_TOKEN: "x",
14
+ },
15
+ secrets: {},
16
+ }),
17
+ ).toBe(BotPlatform.Telegram);
18
+ });
19
+
20
+ test("then the key names, then the token shape", () => {
21
+ expect(
22
+ inferBotPlatform({
23
+ variables: {},
24
+ secrets: {
25
+ DISCORD_BOT_TOKEN: "x",
26
+ },
27
+ }),
28
+ ).toBe(BotPlatform.Discord);
29
+ expect(
30
+ inferBotPlatform({
31
+ variables: {},
32
+ secrets: {
33
+ TOKEN: TELEGRAM_TOKEN,
34
+ },
35
+ }),
36
+ ).toBe(BotPlatform.Telegram);
37
+ });
38
+
39
+ test("answers null when the hints disagree or do not exist", () => {
40
+ expect(
41
+ inferBotPlatform({
42
+ variables: {},
43
+ secrets: {
44
+ DISCORD_TOKEN: "x",
45
+ TELEGRAM_TOKEN: "y",
46
+ },
47
+ }),
48
+ ).toBeNull();
49
+ expect(
50
+ inferBotPlatform({
51
+ variables: {
52
+ BOT_PLATFORM: "slack",
53
+ },
54
+ secrets: {
55
+ API_KEY: "x",
56
+ },
57
+ }),
58
+ ).toBeNull();
59
+ });
60
+ });
@@ -0,0 +1,28 @@
1
+ import { type BotPlatform, isBotPlatform } from "./botPlatform";
2
+ import { type BotEnvironmentMaps, platformOfKey, platformOfToken } from "./botToken";
3
+ import { BOT_PLATFORM, isReservedBotKey } from "./keys";
4
+
5
+ const single = (platforms: (BotPlatform | null)[]) => {
6
+ const found = [
7
+ ...new Set(platforms.filter((platform) => platform !== null)),
8
+ ];
9
+
10
+ return found.length === 1 ? (found[0] ?? null) : null;
11
+ };
12
+
13
+ export const inferBotPlatform = (maps: BotEnvironmentMaps): BotPlatform | null => {
14
+ const explicit = maps.variables[BOT_PLATFORM]?.trim();
15
+
16
+ if (isBotPlatform(explicit)) {
17
+ return explicit;
18
+ }
19
+
20
+ const entries = [
21
+ ...Object.entries(maps.secrets),
22
+ ...Object.entries(maps.variables),
23
+ ].filter(([key]) => !isReservedBotKey(key));
24
+
25
+ return (
26
+ single(entries.map(([key]) => platformOfKey(key))) ?? single(entries.map(([, value]) => platformOfToken(value)))
27
+ );
28
+ };
@@ -0,0 +1,28 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { BOT_PLATFORM_KEYS, BOT_PLATFORM_SECRET_KEYS, BOT_PROTECTED_KEYS, isReservedBotKey } from "./keys";
3
+
4
+ describe("isReservedBotKey", () => {
5
+ test("reserves every platform-owned key, the protected runtime keys and the SERVERK_ prefix", () => {
6
+ for (const key of [
7
+ ...BOT_PLATFORM_KEYS,
8
+ ...BOT_PLATFORM_SECRET_KEYS,
9
+ ...BOT_PROTECTED_KEYS,
10
+ "SERVERK_BOT",
11
+ "SERVERK_ANYTHING",
12
+ ]) {
13
+ expect(isReservedBotKey(key)).toBe(true);
14
+ }
15
+ });
16
+
17
+ test("leaves the customer's own keys alone, case included", () => {
18
+ for (const key of [
19
+ "DISCORD_TOKEN",
20
+ "NODE_OPTIONS",
21
+ "path",
22
+ "serverk_mine",
23
+ "REPO_URL_MIRROR",
24
+ ]) {
25
+ expect(isReservedBotKey(key)).toBe(false);
26
+ }
27
+ });
28
+ });
@@ -0,0 +1,64 @@
1
+ export const SERVERK_PREFIX = "SERVERK_";
2
+
3
+ export const REPO_URL = "REPO_URL";
4
+
5
+ export const REPO_REF = "REPO_REF";
6
+
7
+ export const REPO_BRANCH = "REPO_BRANCH";
8
+
9
+ export const START_COMMAND = "START_COMMAND";
10
+
11
+ export const INSTALL_COMMAND = "INSTALL_COMMAND";
12
+
13
+ export const ROOT_DIRECTORY = "ROOT_DIRECTORY";
14
+
15
+ export const DEPLOYMENT_ID = "DEPLOYMENT_ID";
16
+
17
+ export const SNAPSHOT_ID = "SNAPSHOT_ID";
18
+
19
+ export const BOT_PLATFORM = "BOT_PLATFORM";
20
+
21
+ export const GIT_TOKEN = "GIT_TOKEN";
22
+
23
+ export const SERVERK_BOT = "SERVERK_BOT";
24
+
25
+ export const SERVERK_SERVER_CODE = "SERVERK_SERVER_CODE";
26
+
27
+ export const BOT_PLATFORM_KEYS: readonly string[] = [
28
+ REPO_URL,
29
+ REPO_REF,
30
+ REPO_BRANCH,
31
+ START_COMMAND,
32
+ INSTALL_COMMAND,
33
+ ROOT_DIRECTORY,
34
+ DEPLOYMENT_ID,
35
+ SNAPSHOT_ID,
36
+ BOT_PLATFORM,
37
+ ];
38
+
39
+ export const BOT_PLATFORM_SECRET_KEYS: readonly string[] = [
40
+ GIT_TOKEN,
41
+ ];
42
+
43
+ export const BOT_PROTECTED_KEYS: readonly string[] = [
44
+ "PATH",
45
+ "HOME",
46
+ "VIRTUAL_ENV",
47
+ "PYTHONUNBUFFERED",
48
+ "TZ",
49
+ ];
50
+
51
+ export const BOT_PROVIDED_KEYS: readonly string[] = [
52
+ SERVERK_BOT,
53
+ SERVERK_SERVER_CODE,
54
+ BOT_PLATFORM,
55
+ ];
56
+
57
+ export const isReservedBotKey = (key: string) => {
58
+ return (
59
+ key.startsWith(SERVERK_PREFIX)
60
+ || BOT_PLATFORM_KEYS.includes(key)
61
+ || BOT_PLATFORM_SECRET_KEYS.includes(key)
62
+ || BOT_PROTECTED_KEYS.includes(key)
63
+ );
64
+ };