@serverkgg/bridge 0.0.21 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) 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 +216 -0
  9. package/src/bot/botPanel.ts +121 -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 +210 -2
  50. package/src/cli/validate.ts +71 -7
  51. package/src/context/bridgeContext.ts +11 -0
  52. package/src/core/bridgePort.test.ts +45 -0
  53. package/src/core/bridgePort.ts +26 -0
  54. package/src/guides/compile.ts +2 -2
  55. package/src/manifest/index.ts +1 -0
  56. package/src/manifest/kind.ts +5 -0
  57. package/src/manifest/manifest.test.ts +630 -3
  58. package/src/manifest/manifest.ts +391 -87
  59. package/src/manifest/runtime.ts +2 -2
  60. package/src/manifest/source.ts +4 -4
  61. package/src/modules/bridgeLifecycleSubject.ts +8 -0
  62. package/src/modules/bridgeModule.ts +6 -0
  63. package/src/modules/bridgeStopSignal.ts +13 -0
  64. package/src/modules/index.ts +2 -0
  65. package/src/panel/bridgeDetail.ts +2 -0
  66. package/src/protocol/bridgeConfig.ts +2 -0
  67. package/src/runtime/bridgeConfigReader.test.ts +80 -0
  68. package/src/runtime/bridgeConfigReader.ts +20 -0
  69. package/src/runtime/bridgeContext.test.ts +241 -0
  70. package/src/runtime/bridgeContext.ts +35 -9
  71. package/src/runtime/bridgeDriver.test.ts +132 -1
  72. package/src/runtime/bridgeDriver.ts +54 -1
  73. package/src/runtime/bridgeFiles.test.ts +62 -0
  74. package/src/runtime/bridgeFiles.ts +12 -2
  75. package/src/runtime/bridgeLiveConfig.ts +18 -0
  76. package/src/runtime/bridgeRedaction.test.ts +410 -0
  77. package/src/runtime/bridgeRedaction.ts +214 -0
  78. package/src/runtime/bridgeRegistry.test.ts +3 -0
  79. package/src/runtime/bridgeRegistry.ts +15 -0
  80. package/src/runtime/bridgeServer.test.ts +130 -0
  81. package/src/runtime/bridgeServer.ts +24 -7
  82. package/src/runtime/bridgeSetupHandlers.test.ts +88 -0
  83. package/src/runtime/bridgeSetupHandlers.ts +16 -6
  84. package/src/runtime/bridgeSupervisor.test.ts +670 -2
  85. package/src/runtime/bridgeSupervisor.ts +219 -67
  86. package/src/runtime/serve.ts +47 -13
@@ -0,0 +1,193 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ DEFAULT_REPO_BRANCH,
4
+ DEPLOYMENT_ID_PATTERN,
5
+ isRootDirectory,
6
+ REPO_BRANCH_PATTERN,
7
+ REPO_REF_PATTERN,
8
+ REPO_URL_PATTERN,
9
+ ROOT_DIRECTORY_LENGTH_LIMIT,
10
+ readBotConfig,
11
+ SNAPSHOT_ID_PATTERN,
12
+ } from "./botVariables";
13
+ import {
14
+ BOT_PLATFORM,
15
+ BotPlatform,
16
+ DEPLOYMENT_ID,
17
+ GIT_TOKEN,
18
+ INSTALL_COMMAND,
19
+ REPO_BRANCH,
20
+ REPO_REF,
21
+ REPO_URL,
22
+ ROOT_DIRECTORY,
23
+ SNAPSHOT_ID,
24
+ START_COMMAND,
25
+ } from "./detect";
26
+
27
+ const SHA = "0123456789abcdef0123456789abcdef01234567";
28
+
29
+ const contextWith = (variables: Record<string, string>, secrets: Record<string, string> = {}) => {
30
+ return {
31
+ variable: (key: string) => variables[key] ?? null,
32
+ secret: (key: string) => secrets[key] ?? null,
33
+ };
34
+ };
35
+
36
+ describe("readBotConfig", () => {
37
+ test("reads the platform-owned keys, trims them and takes the git token from the secrets", () => {
38
+ expect(
39
+ readBotConfig(
40
+ contextWith(
41
+ {
42
+ [BOT_PLATFORM]: BotPlatform.Telegram,
43
+ [REPO_URL]: " https://github.com/you/bot ",
44
+ [REPO_REF]: SHA,
45
+ [REPO_BRANCH]: "dev",
46
+ [START_COMMAND]: "node src/bot.js",
47
+ [INSTALL_COMMAND]: "npm install --legacy-peer-deps",
48
+ [ROOT_DIRECTORY]: " apps/bot ",
49
+ [DEPLOYMENT_ID]: "dep_a1b2c3d4",
50
+ [SNAPSHOT_ID]: "snap_1",
51
+ [GIT_TOKEN]: "ignored-in-variables",
52
+ },
53
+ {
54
+ [GIT_TOKEN]: "ghs_installation",
55
+ },
56
+ ),
57
+ ),
58
+ ).toEqual({
59
+ platform: BotPlatform.Telegram,
60
+ repo: "https://github.com/you/bot",
61
+ ref: SHA,
62
+ branch: "dev",
63
+ startCommand: "node src/bot.js",
64
+ installCommand: "npm install --legacy-peer-deps",
65
+ rootDirectory: "apps/bot",
66
+ deployment: "dep_a1b2c3d4",
67
+ snapshot: "snap_1",
68
+ gitToken: "ghs_installation",
69
+ });
70
+ });
71
+
72
+ test("answers nothing for empty values, main for the branch and null for an unknown platform", () => {
73
+ expect(
74
+ readBotConfig(
75
+ contextWith({
76
+ [BOT_PLATFORM]: "slack",
77
+ [REPO_URL]: " ",
78
+ }),
79
+ ),
80
+ ).toEqual({
81
+ platform: null,
82
+ repo: null,
83
+ ref: null,
84
+ branch: DEFAULT_REPO_BRANCH,
85
+ startCommand: null,
86
+ installCommand: null,
87
+ rootDirectory: null,
88
+ deployment: null,
89
+ snapshot: null,
90
+ gitToken: null,
91
+ });
92
+ });
93
+ });
94
+
95
+ describe("the patterns", () => {
96
+ test("pin a ref only to a full sha-1 or sha-256", () => {
97
+ expect(REPO_REF_PATTERN.test(SHA)).toBe(true);
98
+ expect(REPO_REF_PATTERN.test("a".repeat(64))).toBe(true);
99
+ expect(REPO_REF_PATTERN.test(SHA.slice(0, 12))).toBe(false);
100
+ expect(REPO_REF_PATTERN.test(SHA.toUpperCase())).toBe(false);
101
+ expect(REPO_REF_PATTERN.test("main")).toBe(false);
102
+ });
103
+
104
+ test("keep deployment and snapshot ids to url-safe words", () => {
105
+ for (const pattern of [
106
+ DEPLOYMENT_ID_PATTERN,
107
+ SNAPSHOT_ID_PATTERN,
108
+ ]) {
109
+ expect(pattern.test("dep_a1b2c3d4")).toBe(true);
110
+ expect(pattern.test("0f8e-uuid-like")).toBe(true);
111
+ expect(pattern.test("a".repeat(65))).toBe(false);
112
+ expect(pattern.test("../escape")).toBe(false);
113
+ expect(pattern.test("")).toBe(false);
114
+ }
115
+ });
116
+
117
+ test("take a root directory only as a plain relative folder path, the shape the api enforces", () => {
118
+ for (const root of [
119
+ "bot",
120
+ "apps/bot",
121
+ "packages/my-bot_v2",
122
+ "src.bot/v1.2",
123
+ "a".repeat(ROOT_DIRECTORY_LENGTH_LIMIT),
124
+ ]) {
125
+ expect(isRootDirectory(root)).toBe(true);
126
+ }
127
+
128
+ for (const root of [
129
+ "",
130
+ "/bot",
131
+ "./bot",
132
+ ".bot",
133
+ "..",
134
+ "apps/../bot",
135
+ "apps/./bot",
136
+ "apps/..",
137
+ "apps//bot",
138
+ "apps/bot/",
139
+ "apps\\bot",
140
+ "apps\u0000bot",
141
+ "apps bot",
142
+ "a".repeat(ROOT_DIRECTORY_LENGTH_LIMIT + 1),
143
+ ]) {
144
+ expect(isRootDirectory(root)).toBe(false);
145
+ }
146
+ });
147
+
148
+ test("accept https repositories and git branch names", () => {
149
+ expect(REPO_URL_PATTERN.test("https://github.com/you/bot.git")).toBe(true);
150
+ expect(REPO_URL_PATTERN.test("https://user:pass@github.com/you/bot")).toBe(false);
151
+ expect(REPO_URL_PATTERN.test("git@github.com:you/bot.git")).toBe(false);
152
+ expect(REPO_BRANCH_PATTERN.test("feature/new-bot")).toBe(true);
153
+ expect(REPO_BRANCH_PATTERN.test("-rf")).toBe(false);
154
+ });
155
+
156
+ test("accept only branch names git check-ref-format accepts", () => {
157
+ for (const branch of [
158
+ "main",
159
+ "release-1.2",
160
+ "a.locked",
161
+ "deps/HEAD",
162
+ "x".repeat(128),
163
+ ]) {
164
+ expect(REPO_BRANCH_PATTERN.test(branch), branch).toBe(true);
165
+ }
166
+
167
+ for (const branch of [
168
+ "HEAD",
169
+ "a..b",
170
+ "a//b",
171
+ "/main",
172
+ "main/",
173
+ "main.",
174
+ "a/.hidden",
175
+ ".hidden",
176
+ "main.lock",
177
+ "a.lock/b",
178
+ "a@{1}",
179
+ "a\\b",
180
+ "a~1",
181
+ "a^2",
182
+ "a:b",
183
+ "a?b",
184
+ "a*b",
185
+ "a[b",
186
+ "with space",
187
+ "tab\there",
188
+ "x".repeat(129),
189
+ ]) {
190
+ expect(REPO_BRANCH_PATTERN.test(branch), JSON.stringify(branch)).toBe(false);
191
+ }
192
+ });
193
+ });
@@ -0,0 +1,73 @@
1
+ import type { BridgeContext } from "../context";
2
+ import {
3
+ BOT_PLATFORM,
4
+ type BotPlatform,
5
+ DEPLOYMENT_ID,
6
+ GIT_TOKEN,
7
+ INSTALL_COMMAND,
8
+ isBotPlatform,
9
+ REPO_BRANCH,
10
+ REPO_REF,
11
+ REPO_URL,
12
+ ROOT_DIRECTORY,
13
+ SNAPSHOT_ID,
14
+ START_COMMAND,
15
+ } from "./detect";
16
+
17
+ export const REPO_URL_PATTERN = /^https:\/\/[^\s@/]+\/\S+$/;
18
+
19
+ export const REPO_BRANCH_PATTERN =
20
+ /^(?!HEAD$)(?!.*(?:\.\.|\/\/|\/\.|\.lock(?:\/|$)|[/.]$))[A-Za-z0-9_][A-Za-z0-9._/-]{0,127}$/;
21
+
22
+ export const REPO_REF_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
23
+
24
+ export const DEPLOYMENT_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
25
+
26
+ export const SNAPSHOT_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
27
+
28
+ export const ROOT_DIRECTORY_PATTERN =
29
+ /^(?![./])(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?!.*\/\/)[A-Za-z0-9._\-/]*[A-Za-z0-9._-]$/;
30
+
31
+ export const ROOT_DIRECTORY_LENGTH_LIMIT = 255;
32
+
33
+ export const DEFAULT_REPO_BRANCH = "main";
34
+
35
+ export const isRootDirectory = (value: string) => {
36
+ return value.length <= ROOT_DIRECTORY_LENGTH_LIMIT && ROOT_DIRECTORY_PATTERN.test(value);
37
+ };
38
+
39
+ export interface BotConfig {
40
+ platform: BotPlatform | null;
41
+ repo: string | null;
42
+ ref: string | null;
43
+ branch: string;
44
+ startCommand: string | null;
45
+ installCommand: string | null;
46
+ rootDirectory: string | null;
47
+ deployment: string | null;
48
+ snapshot: string | null;
49
+ gitToken: string | null;
50
+ }
51
+
52
+ const present = (value: string | null) => {
53
+ const trimmed = value?.trim() ?? "";
54
+
55
+ return trimmed.length === 0 ? null : trimmed;
56
+ };
57
+
58
+ export const readBotConfig = (context: Pick<BridgeContext, "variable" | "secret">): BotConfig => {
59
+ const platform = present(context.variable(BOT_PLATFORM));
60
+
61
+ return {
62
+ platform: isBotPlatform(platform) ? platform : null,
63
+ repo: present(context.variable(REPO_URL)),
64
+ ref: present(context.variable(REPO_REF)),
65
+ branch: present(context.variable(REPO_BRANCH)) ?? DEFAULT_REPO_BRANCH,
66
+ startCommand: present(context.variable(START_COMMAND)),
67
+ installCommand: present(context.variable(INSTALL_COMMAND)),
68
+ rootDirectory: present(context.variable(ROOT_DIRECTORY)),
69
+ deployment: present(context.variable(DEPLOYMENT_ID)),
70
+ snapshot: present(context.variable(SNAPSHOT_ID)),
71
+ gitToken: present(context.secret(GIT_TOKEN)),
72
+ };
73
+ };
@@ -0,0 +1,23 @@
1
+ export enum BotFramework {
2
+ DiscordJs = "discord-js",
3
+ Eris = "eris",
4
+ OceanicJs = "oceanic-js",
5
+ Grammy = "grammy",
6
+ Telegraf = "telegraf",
7
+ NodeTelegramBotApi = "node-telegram-bot-api",
8
+ DiscordPy = "discord-py",
9
+ PyCord = "py-cord",
10
+ Nextcord = "nextcord",
11
+ Disnake = "disnake",
12
+ Hikari = "hikari",
13
+ PythonTelegramBot = "python-telegram-bot",
14
+ Aiogram = "aiogram",
15
+ Pyrogram = "pyrogram",
16
+ Telethon = "telethon",
17
+ }
18
+
19
+ const FRAMEWORKS: ReadonlySet<string> = new Set(Object.values(BotFramework));
20
+
21
+ export const isBotFramework = (value: string | null | undefined): value is BotFramework => {
22
+ return typeof value === "string" && FRAMEWORKS.has(value);
23
+ };
@@ -0,0 +1,198 @@
1
+ import { BotFramework } from "./botFramework";
2
+ import { BotPlatform } from "./botPlatform";
3
+ import { BotRuntime } from "./botRuntime";
4
+
5
+ export interface BotFrameworkSpec {
6
+ framework: BotFramework;
7
+ name: string;
8
+ runtime: BotRuntime;
9
+ platform: BotPlatform;
10
+ packages: readonly string[];
11
+ modules: readonly string[];
12
+ }
13
+
14
+ export const BOT_FRAMEWORKS: readonly BotFrameworkSpec[] = [
15
+ {
16
+ framework: BotFramework.DiscordJs,
17
+ name: "discord.js",
18
+ runtime: BotRuntime.Node,
19
+ platform: BotPlatform.Discord,
20
+ packages: [
21
+ "discord.js",
22
+ ],
23
+ modules: [
24
+ "discord.js",
25
+ ],
26
+ },
27
+ {
28
+ framework: BotFramework.Eris,
29
+ name: "Eris",
30
+ runtime: BotRuntime.Node,
31
+ platform: BotPlatform.Discord,
32
+ packages: [
33
+ "eris",
34
+ ],
35
+ modules: [
36
+ "eris",
37
+ ],
38
+ },
39
+ {
40
+ framework: BotFramework.OceanicJs,
41
+ name: "Oceanic.js",
42
+ runtime: BotRuntime.Node,
43
+ platform: BotPlatform.Discord,
44
+ packages: [
45
+ "oceanic.js",
46
+ ],
47
+ modules: [
48
+ "oceanic.js",
49
+ ],
50
+ },
51
+ {
52
+ framework: BotFramework.Grammy,
53
+ name: "grammY",
54
+ runtime: BotRuntime.Node,
55
+ platform: BotPlatform.Telegram,
56
+ packages: [
57
+ "grammy",
58
+ ],
59
+ modules: [
60
+ "grammy",
61
+ ],
62
+ },
63
+ {
64
+ framework: BotFramework.Telegraf,
65
+ name: "Telegraf",
66
+ runtime: BotRuntime.Node,
67
+ platform: BotPlatform.Telegram,
68
+ packages: [
69
+ "telegraf",
70
+ ],
71
+ modules: [
72
+ "telegraf",
73
+ ],
74
+ },
75
+ {
76
+ framework: BotFramework.NodeTelegramBotApi,
77
+ name: "node-telegram-bot-api",
78
+ runtime: BotRuntime.Node,
79
+ platform: BotPlatform.Telegram,
80
+ packages: [
81
+ "node-telegram-bot-api",
82
+ ],
83
+ modules: [
84
+ "node-telegram-bot-api",
85
+ ],
86
+ },
87
+ {
88
+ framework: BotFramework.DiscordPy,
89
+ name: "discord.py",
90
+ runtime: BotRuntime.Python,
91
+ platform: BotPlatform.Discord,
92
+ packages: [
93
+ "discord.py",
94
+ "discord",
95
+ ],
96
+ modules: [
97
+ "discord",
98
+ ],
99
+ },
100
+ {
101
+ framework: BotFramework.PyCord,
102
+ name: "Pycord",
103
+ runtime: BotRuntime.Python,
104
+ platform: BotPlatform.Discord,
105
+ packages: [
106
+ "py-cord",
107
+ ],
108
+ modules: [],
109
+ },
110
+ {
111
+ framework: BotFramework.Nextcord,
112
+ name: "nextcord",
113
+ runtime: BotRuntime.Python,
114
+ platform: BotPlatform.Discord,
115
+ packages: [
116
+ "nextcord",
117
+ ],
118
+ modules: [
119
+ "nextcord",
120
+ ],
121
+ },
122
+ {
123
+ framework: BotFramework.Disnake,
124
+ name: "disnake",
125
+ runtime: BotRuntime.Python,
126
+ platform: BotPlatform.Discord,
127
+ packages: [
128
+ "disnake",
129
+ ],
130
+ modules: [
131
+ "disnake",
132
+ ],
133
+ },
134
+ {
135
+ framework: BotFramework.Hikari,
136
+ name: "hikari",
137
+ runtime: BotRuntime.Python,
138
+ platform: BotPlatform.Discord,
139
+ packages: [
140
+ "hikari",
141
+ ],
142
+ modules: [
143
+ "hikari",
144
+ ],
145
+ },
146
+ {
147
+ framework: BotFramework.PythonTelegramBot,
148
+ name: "python-telegram-bot",
149
+ runtime: BotRuntime.Python,
150
+ platform: BotPlatform.Telegram,
151
+ packages: [
152
+ "python-telegram-bot",
153
+ ],
154
+ modules: [
155
+ "telegram",
156
+ ],
157
+ },
158
+ {
159
+ framework: BotFramework.Aiogram,
160
+ name: "aiogram",
161
+ runtime: BotRuntime.Python,
162
+ platform: BotPlatform.Telegram,
163
+ packages: [
164
+ "aiogram",
165
+ ],
166
+ modules: [
167
+ "aiogram",
168
+ ],
169
+ },
170
+ {
171
+ framework: BotFramework.Pyrogram,
172
+ name: "Pyrogram",
173
+ runtime: BotRuntime.Python,
174
+ platform: BotPlatform.Telegram,
175
+ packages: [
176
+ "pyrogram",
177
+ ],
178
+ modules: [
179
+ "pyrogram",
180
+ ],
181
+ },
182
+ {
183
+ framework: BotFramework.Telethon,
184
+ name: "Telethon",
185
+ runtime: BotRuntime.Python,
186
+ platform: BotPlatform.Telegram,
187
+ packages: [
188
+ "telethon",
189
+ ],
190
+ modules: [
191
+ "telethon",
192
+ ],
193
+ },
194
+ ];
195
+
196
+ export const botFrameworkSpec = (framework: BotFramework): BotFrameworkSpec | null => {
197
+ return BOT_FRAMEWORKS.find((spec) => spec.framework === framework) ?? null;
198
+ };
@@ -0,0 +1,15 @@
1
+ export enum BotInspectWarning {
2
+ NoLockfile = "no-lockfile",
3
+ NoStart = "no-start",
4
+ NoManifest = "no-manifest",
5
+ TypescriptEntry = "typescript-entry",
6
+ EnvCommitted = "env-committed",
7
+ NodeModulesCommitted = "node-modules-committed",
8
+ VenvCommitted = "venv-committed",
9
+ UnpinnedRequirements = "unpinned-requirements",
10
+ TwoRuntimes = "two-runtimes",
11
+ TwoPlatforms = "two-platforms",
12
+ ManifestDisagrees = "manifest-disagrees",
13
+ ManifestInvalid = "manifest-invalid",
14
+ ReservedEnvKey = "reserved-env-key",
15
+ }
@@ -0,0 +1,6 @@
1
+ export enum BotPackageManager {
2
+ Npm = "npm",
3
+ Pnpm = "pnpm",
4
+ Yarn = "yarn",
5
+ Pip = "pip",
6
+ }
@@ -0,0 +1,10 @@
1
+ export enum BotPlatform {
2
+ Discord = "discord",
3
+ Telegram = "telegram",
4
+ }
5
+
6
+ const PLATFORMS: ReadonlySet<string> = new Set(Object.values(BotPlatform));
7
+
8
+ export const isBotPlatform = (value: string | null | undefined): value is BotPlatform => {
9
+ return typeof value === "string" && PLATFORMS.has(value);
10
+ };
@@ -0,0 +1,15 @@
1
+ export enum BotRuntime {
2
+ Node = "node",
3
+ Python = "python",
4
+ }
5
+
6
+ export const BOT_RUNTIME_PACKAGES: Readonly<Record<BotRuntime, string>> = {
7
+ [BotRuntime.Node]: "bot-node",
8
+ [BotRuntime.Python]: "bot-python",
9
+ };
10
+
11
+ const RUNTIMES: ReadonlySet<string> = new Set(Object.values(BotRuntime));
12
+
13
+ export const isBotRuntime = (value: string | null | undefined): value is BotRuntime => {
14
+ return typeof value === "string" && RUNTIMES.has(value);
15
+ };
@@ -0,0 +1,71 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { BotPlatform } from "./botPlatform";
3
+ import { discordApplicationId, findBotToken, platformOfKey, platformOfToken, telegramBotId } from "./botToken";
4
+
5
+ const DISCORD_ID = "1136470478027245683";
6
+
7
+ const DISCORD_TOKEN = `${btoa(DISCORD_ID).replaceAll("=", "")}.GxYz12.${"a".repeat(38)}`;
8
+
9
+ const TELEGRAM_TOKEN = "7123456789:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw";
10
+
11
+ describe("token shapes", () => {
12
+ test("read the ids hidden in each platform's token", () => {
13
+ expect(telegramBotId(TELEGRAM_TOKEN)).toBe("7123456789");
14
+ expect(discordApplicationId(` ${DISCORD_TOKEN} `)).toBe(DISCORD_ID);
15
+ expect(platformOfToken(TELEGRAM_TOKEN)).toBe(BotPlatform.Telegram);
16
+ expect(platformOfToken(DISCORD_TOKEN)).toBe(BotPlatform.Discord);
17
+ });
18
+
19
+ test("do not mistake a JSON web token or a random key for a bot token", () => {
20
+ const jwt = `${btoa('{"alg":"HS256"}').replaceAll("=", "")}.eyJzdWIiOiIxIn0.${"b".repeat(43)}`;
21
+
22
+ expect(platformOfToken(jwt)).toBeNull();
23
+ expect(platformOfToken("sk-live-0123456789abcdef")).toBeNull();
24
+ expect(platformOfToken("")).toBeNull();
25
+ });
26
+
27
+ test("read the platform a key name points at", () => {
28
+ expect(platformOfKey("DISCORD_TOKEN")).toBe(BotPlatform.Discord);
29
+ expect(platformOfKey("my_telegram_chat")).toBe(BotPlatform.Telegram);
30
+ expect(platformOfKey("TOKEN")).toBeNull();
31
+ });
32
+ });
33
+
34
+ describe("findBotToken", () => {
35
+ test("prefers the tutorial names, then any value shaped like the platform's token", () => {
36
+ expect(
37
+ findBotToken(BotPlatform.Discord, {
38
+ variables: {},
39
+ secrets: {
40
+ OTHER: DISCORD_TOKEN,
41
+ DISCORD_TOKEN,
42
+ },
43
+ }),
44
+ ).toEqual({
45
+ key: "DISCORD_TOKEN",
46
+ value: DISCORD_TOKEN,
47
+ });
48
+ expect(
49
+ findBotToken(BotPlatform.Telegram, {
50
+ variables: {
51
+ TG: TELEGRAM_TOKEN,
52
+ },
53
+ secrets: {},
54
+ }),
55
+ ).toEqual({
56
+ key: "TG",
57
+ value: TELEGRAM_TOKEN,
58
+ });
59
+ });
60
+
61
+ test("ignores a named key whose value is not the platform's token", () => {
62
+ expect(
63
+ findBotToken(BotPlatform.Telegram, {
64
+ variables: {},
65
+ secrets: {
66
+ TELEGRAM_TOKEN: "not-a-token",
67
+ },
68
+ }),
69
+ ).toBeNull();
70
+ });
71
+ });