@serverkgg/bridge 0.1.5 → 0.1.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverkgg/bridge",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Serverk game bridge and driver runtime, wire protocol, manifest schema, and maintainer CLI",
5
5
  "homepage": "https://serverk.gg",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -39,9 +39,9 @@
39
39
  },
40
40
  "scripts": {
41
41
  "tsc": "tsc",
42
- "test": "bun test --parallel"
42
+ "test": "bun test --parallel=8"
43
43
  },
44
44
  "dependencies": {
45
- "zod": "4.6.4"
45
+ "zod": "4.6.5"
46
46
  }
47
47
  }
@@ -13,6 +13,10 @@ const CASES: readonly {
13
13
  line: "Error [TokenInvalid]: An invalid token was provided.",
14
14
  cause: BridgeCrashCause.TokenInvalid,
15
15
  },
16
+ {
17
+ line: "Error [TOKEN_INVALID]: An invalid token was provided.",
18
+ cause: BridgeCrashCause.TokenInvalid,
19
+ },
16
20
  {
17
21
  line: "discord.errors.LoginFailure: Improper token has been passed.",
18
22
  cause: BridgeCrashCause.TokenInvalid,
@@ -37,6 +41,46 @@ const CASES: readonly {
37
41
  line: "aiogram.exceptions.TelegramUnauthorizedError: Telegram server says - Unauthorized",
38
42
  cause: BridgeCrashCause.TokenInvalid,
39
43
  },
44
+ {
45
+ line: "pyrogram.errors.exceptions.bad_request_400.AccessTokenInvalid: Telegram says: [400 ACCESS_TOKEN_INVALID] - The bot access token is invalid",
46
+ cause: BridgeCrashCause.TokenInvalid,
47
+ },
48
+ {
49
+ line: "pyrogram.errors.exceptions.bad_request_400.AccessTokenExpired: Telegram says: [400 ACCESS_TOKEN_EXPIRED] - The bot token has expired",
50
+ cause: BridgeCrashCause.TokenInvalid,
51
+ },
52
+ {
53
+ line: "TypeError: expected token to be a str, received NoneType instead",
54
+ cause: BridgeCrashCause.TokenInvalid,
55
+ },
56
+ {
57
+ line: "TypeError: expected token to be a str, received <class 'NoneType'> instead",
58
+ cause: BridgeCrashCause.TokenInvalid,
59
+ },
60
+ {
61
+ line: "TypeError: token must be of type str, not NoneType",
62
+ cause: BridgeCrashCause.TokenInvalid,
63
+ },
64
+ {
65
+ line: "TypeError: token must be of type str, got NoneType instead",
66
+ cause: BridgeCrashCause.TokenInvalid,
67
+ },
68
+ {
69
+ line: "TypeError: The token provided was of type <class 'NoneType'> but was expected to be str",
70
+ cause: BridgeCrashCause.TokenInvalid,
71
+ },
72
+ {
73
+ line: "Error: Empty token!",
74
+ cause: BridgeCrashCause.TokenInvalid,
75
+ },
76
+ {
77
+ line: "aiogram.utils.token.TokenValidationError: Token is invalid!",
78
+ cause: BridgeCrashCause.TokenInvalid,
79
+ },
80
+ {
81
+ line: "aiogram.utils.token.TokenValidationError: Token is invalid! It must be 'str' type instead of <class 'NoneType'> type.",
82
+ cause: BridgeCrashCause.TokenInvalid,
83
+ },
40
84
  {
41
85
  line: "Error: Used disallowed intents",
42
86
  cause: BridgeCrashCause.IntentsDisallowed,
@@ -45,6 +89,10 @@ const CASES: readonly {
45
89
  line: "Error [DisallowedIntents]: Privileged intent provided is not enabled or whitelisted.",
46
90
  cause: BridgeCrashCause.IntentsDisallowed,
47
91
  },
92
+ {
93
+ line: "Error [DISALLOWED_INTENTS]: Privileged intent provided is not enabled or whitelisted.",
94
+ cause: BridgeCrashCause.IntentsDisallowed,
95
+ },
48
96
  {
49
97
  line: "discord.errors.PrivilegedIntentsRequired: Shard ID None is requesting privileged intents that have not been explicitly enabled in the developer portal.",
50
98
  cause: BridgeCrashCause.IntentsDisallowed,
@@ -257,6 +305,14 @@ describe("BOT_CRASH_EVIDENCE", () => {
257
305
  expect(ring.crash(recordOf())).toBe(BridgeCrashCause.Unknown);
258
306
  });
259
307
 
308
+ it("leaves a TypeError about something other than the token unexplained", () => {
309
+ const { ring, print } = botRing();
310
+
311
+ print("TypeError: expected str, bytes or os.PathLike object, not NoneType");
312
+
313
+ expect(ring.crash(recordOf())).toBe(BridgeCrashCause.Unknown);
314
+ });
315
+
260
316
  it("calls exit 137 out of memory only when the record carries the flag", () => {
261
317
  const unflagged = botRing();
262
318
 
@@ -4,7 +4,7 @@ import { BridgeEventName } from "../protocol";
4
4
  export const BOT_CRASH_EVIDENCE: readonly BridgeEventPattern[] = [
5
5
  {
6
6
  match:
7
- /\[TokenInvalid\]|An invalid token was provided|LoginFailure: Improper token|\b401:? Unauthorized\b|\bInvalidToken\b|TelegramUnauthorizedError/,
7
+ /\[TokenInvalid\]|An invalid token was provided|LoginFailure: Improper token|\b401:? Unauthorized\b|\bInvalidToken\b|TelegramUnauthorizedError|\bACCESS_TOKEN_(?:INVALID|EXPIRED)\b/,
8
8
  emit: BridgeEventName.TokenInvalid,
9
9
  },
10
10
  {
@@ -13,7 +13,13 @@ export const BOT_CRASH_EVIDENCE: readonly BridgeEventPattern[] = [
13
13
  emit: BridgeEventName.TokenInvalid,
14
14
  },
15
15
  {
16
- match: /Used disallowed intents|\[DisallowedIntents\]|PrivilegedIntentsRequired|close code:? 4014\b/,
16
+ match:
17
+ /TypeError: (?:expected token to be a str, received|token must be of type str, (?:not|got) |The token provided was of type .+ but was expected to be str)|\bError: Empty token!|TokenValidationError: Token is invalid!/,
18
+ emit: BridgeEventName.TokenInvalid,
19
+ },
20
+ {
21
+ match:
22
+ /Used disallowed intents|\[DisallowedIntents\]|Privileged intent provided is not enabled or whitelisted|PrivilegedIntentsRequired|close code:? 4014\b/,
17
23
  emit: BridgeEventName.IntentsDisallowed,
18
24
  },
19
25
  {
@@ -174,6 +174,7 @@ export const BOT_FRAMEWORKS: readonly BotFrameworkSpec[] = [
174
174
  platform: BotPlatform.Telegram,
175
175
  packages: [
176
176
  "pyrogram",
177
+ "kurigram",
177
178
  ],
178
179
  modules: [
179
180
  "pyrogram",
@@ -3,6 +3,7 @@ export enum BotInspectWarning {
3
3
  NodeVersionUnmet = "node-version-unmet",
4
4
  LockfileNotHonoured = "lockfile-not-honoured",
5
5
  NoStart = "no-start",
6
+ WindowsCommand = "windows-command",
6
7
  NoManifest = "no-manifest",
7
8
  EnvCommitted = "env-committed",
8
9
  NodeModulesCommitted = "node-modules-committed",
@@ -35,8 +35,8 @@
35
35
  "expected": {
36
36
  "runtime": "python",
37
37
  "packageManager": "pip",
38
- "framework": null,
39
- "platform": null,
38
+ "framework": "pyrogram",
39
+ "platform": "telegram",
40
40
  "install": "uv pip install -r requirements.txt",
41
41
  "build": null,
42
42
  "start": "python -m bot",
@@ -18,6 +18,7 @@
18
18
  "yarn.lock"
19
19
  ],
20
20
  "contents": {
21
+ ".env-example": "DISCORD_TOKEN=\nOWNER=",
21
22
  "package.json": "{\n \"name\": \"guidebot\",\n \"version\": \"2.1.0\",\n \"description\": \"A boilerplate example bot with command handler and reloadable commands. Updated and Maintained by the Idiot's Guide Community\",\n \"main\": \"index.js\",\n \"scripts\": {\n \"start\": \"node index.js\"\n },\n \"engines\": {\n \"node\": \">=16\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/AnIdiotsGuide/guidebot.git\"\n },\n \"author\": \"Evelyne Lachance <eslachance@gmail.com> (http://evie.codes)\",\n \"contributors\": [\n \"York (http://anidiots.guide)\"\n ],\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/AnIdiotsGuide/guidebot/issues\"\n },\n \"homepage\": \"https://github.com/AnIdiotsGuide/guidebot#readme\",\n \"dependencies\": {\n \"@discordjs/builders\": \"^0.2.0\",\n \"@sapphire/time-utilities\": \"^1.3.8\",\n \"colorette\": \"^1.3.0\",\n \"discord.js\": \"^13.0.1\",\n \"dotenv\": \"^10.0.0\",\n \"enmap\": \"^5.8.5\"\n },\n \"devDependencies\": {\n \"eslint\": \"^7.30.0\",\n \"eslint-config-airbnb-base\": \"^14.2.1\",\n \"eslint-config-standard\": \"^16.0.3\",\n \"eslint-plugin-import\": \"^2.23.4\",\n \"eslint-plugin-node\": \"^11.1.0\",\n \"eslint-plugin-promise\": \"^5.1.0\"\n }\n}\n"
22
23
  },
23
24
  "expected": {
@@ -30,7 +31,7 @@
30
31
  "start": "node index.js",
31
32
  "nodeVersion": "16",
32
33
  "pythonVersion": null,
33
- "tokenKey": null,
34
+ "tokenKey": "DISCORD_TOKEN",
34
35
  "warnings": []
35
36
  }
36
37
  }
@@ -32,8 +32,8 @@
32
32
  "expected": {
33
33
  "runtime": "python",
34
34
  "packageManager": "pip",
35
- "framework": null,
36
- "platform": null,
35
+ "framework": "pyrogram",
36
+ "platform": "telegram",
37
37
  "install": "uv pip install -r requirements.txt",
38
38
  "build": null,
39
39
  "start": "bash start",
@@ -17,6 +17,7 @@
17
17
  "requirements.txt"
18
18
  ],
19
19
  "contents": {
20
+ ".env.dist": "BOT_TOKEN=123456:Your-TokEn_ExaMple\nADMINS=123456,654321\nUSE_REDIS=False\n\n#POSTGRES_USER=someusername\n#POSTGRES_PASSWORD=postgres_pass12345\n#POSTGRES_DB=bot\n#DB_HOST=pg_database\n\n# REDIS_HOST=redis_cache\n# REDIS_PORT=6388\n# REDIS_DB=1\n# REDIS_PASSWORD=someredispass\n\n\n#WEBHOOK_EXPOSE=8001\n#WEBHOOK_APP_NAME=webhook\n",
20
21
  "requirements.txt": "aiogram~=3.0\nenvirons~=9.0\nredis\nbetterlogging\n\n# # For enabling api:\n# backoff\n# ujson\n# yarl\n\n# # For PostgreSQL sqlalchemy + alembic:\n# sqlalchemy~=2.0\n# alembic~=1.0\n# asyncpg\n"
21
22
  },
22
23
  "expected": {
@@ -29,7 +30,7 @@
29
30
  "start": "python bot.py",
30
31
  "nodeVersion": null,
31
32
  "pythonVersion": null,
32
- "tokenKey": null,
33
+ "tokenKey": "BOT_TOKEN",
33
34
  "warnings": [
34
35
  "unpinned-requirements"
35
36
  ]
@@ -28,6 +28,8 @@
28
28
  "nodeVersion": null,
29
29
  "pythonVersion": null,
30
30
  "tokenKey": null,
31
- "warnings": []
31
+ "warnings": [
32
+ "windows-command"
33
+ ]
32
34
  }
33
35
  }
@@ -21,6 +21,7 @@
21
21
  "tsconfig.json"
22
22
  ],
23
23
  "contents": {
24
+ ".env-sample": "BOT_TOKEN=\"\"\nUSERNAME=\"\"\nWEBHOOK_URL=\"\"\nPORT=\"3000\"\nNODE_ENV=\"development\"\n",
24
25
  "package.json": "{\n \"name\": \"telegram-bot-boilerplate\",\n \"version\": \"1.0.0\",\n \"description\": \"Telegram Bot Boilerplate\",\n \"author\": \"Yuri Nezdemkovski <yuri@nezdemkovski.com> (https://nezdemkovski.com/)\",\n \"homepage\": \"https://github.com/nezdemkovski/telegram-bot-boilerplate\",\n \"license\": \"MIT\",\n \"engines\": {\n \"bun\": \">=1\"\n },\n \"dependencies\": {\n \"grammy\": \"^1.40.0\"\n },\n \"devDependencies\": {\n \"bun-types\": \"1.3.9\",\n \"eslint\": \"^10.0.1\",\n \"eslint-config-prettier\": \"^10.1.8\",\n \"prettier\": \"^3.8.1\",\n \"typescript\": \"^5.9.3\",\n \"typescript-eslint\": \"^8.56.0\"\n },\n \"scripts\": {\n \"dev\": \"bun --watch src/index.ts\",\n \"build\": \"bun build src/index.ts --outdir dist --target bun --minify\",\n \"start\": \"NODE_ENV=production bun dist/index.js\",\n \"typecheck\": \"tsc --noEmit\",\n \"lint\": \"eslint src\",\n \"format\": \"prettier --write 'src/**/*.ts'\"\n }\n}\n"
25
26
  },
26
27
  "expected": {
@@ -33,7 +34,7 @@
33
34
  "start": "NODE_ENV=production bun dist/index.js",
34
35
  "nodeVersion": null,
35
36
  "pythonVersion": null,
36
- "tokenKey": null,
37
+ "tokenKey": "BOT_TOKEN",
37
38
  "warnings": []
38
39
  }
39
40
  }
@@ -28,8 +28,8 @@
28
28
  "expected": {
29
29
  "runtime": "python",
30
30
  "packageManager": "pip",
31
- "framework": null,
32
- "platform": null,
31
+ "framework": "pyrogram",
32
+ "platform": "telegram",
33
33
  "install": "uv pip install -r requirements.txt",
34
34
  "build": null,
35
35
  "start": "python src/main.py",
@@ -604,6 +604,24 @@ describe("detect warnings", () => {
604
604
  ]);
605
605
  });
606
606
 
607
+ it("warns when a start or build script runs a Windows-only command", () => {
608
+ const scripts = (start: string, build: string) => {
609
+ return project({
610
+ "package.json": JSON.stringify({
611
+ scripts: {
612
+ start,
613
+ build,
614
+ },
615
+ }),
616
+ "package-lock.json": "{}",
617
+ }).warnings;
618
+ };
619
+
620
+ expect(scripts("cls && node build/index.js", "cls && node build.js")).toContain(BotInspectWarning.WindowsCommand);
621
+ expect(scripts("node build/index.js", "CLS&&node build.js")).toContain(BotInspectWarning.WindowsCommand);
622
+ expect(scripts("node index.js", "tsc")).not.toContain(BotInspectWarning.WindowsCommand);
623
+ });
624
+
607
625
  it("flags two runtimes and picks the one whose framework it found", () => {
608
626
  const result = project({
609
627
  ...DISCORD_NODE,
@@ -630,6 +648,17 @@ describe("detect warnings", () => {
630
648
  expect(result.warnings).toContain(BotInspectWarning.TwoPlatforms);
631
649
  });
632
650
 
651
+ it("reads kurigram, the Pyrogram fork the corpus bots install, as Pyrogram on Telegram", () => {
652
+ const result = project({
653
+ "requirements.txt": "kurigram>=2.2.25\ntgcrypto\n",
654
+ "bot/__main__.py": "",
655
+ });
656
+
657
+ expect(result.framework).toBe(BotFramework.Pyrogram);
658
+ expect(result.platform).toBe(BotPlatform.Telegram);
659
+ expect(result.warnings).not.toContain(BotInspectWarning.TwoPlatforms);
660
+ });
661
+
633
662
  it("drops keys the platform owns and warns, but keeps quiet about keys the runtime gives anyway", () => {
634
663
  const result = project({
635
664
  ...DISCORD_NODE,
@@ -784,6 +813,45 @@ describe("detect env suggestions", () => {
784
813
  ]);
785
814
  });
786
815
 
816
+ const CASES: readonly {
817
+ file: string;
818
+ }[] = [
819
+ {
820
+ file: ".env-example",
821
+ },
822
+ {
823
+ file: ".env-sample",
824
+ },
825
+ {
826
+ file: ".env.dist",
827
+ },
828
+ ];
829
+
830
+ for (const item of CASES) {
831
+ it(`reads ${item.file} the way it reads .env.example`, () => {
832
+ const result = project({
833
+ "package.json": "{}",
834
+ [item.file]: "DISCORD_TOKEN=\nOWNER=\n",
835
+ });
836
+
837
+ expect(result.env).toEqual([
838
+ {
839
+ key: "DISCORD_TOKEN",
840
+ sensitive: true,
841
+ source: item.file,
842
+ example: null,
843
+ },
844
+ {
845
+ key: "OWNER",
846
+ sensitive: false,
847
+ source: item.file,
848
+ example: null,
849
+ },
850
+ ]);
851
+ expect(result.tokenKey).toBe("DISCORD_TOKEN");
852
+ });
853
+ }
854
+
787
855
  it("suggests the keys of a skipped .env after the example files, never its values", () => {
788
856
  const result = detect({
789
857
  files: [
@@ -53,6 +53,12 @@ const NOT_A_BOT_PACKAGE: ReadonlySet<string> = new Set([
53
53
  "scripts",
54
54
  ]);
55
55
 
56
+ const WINDOWS_COMMANDS: ReadonlySet<string> = new Set([
57
+ "cls",
58
+ ]);
59
+
60
+ const COMMAND_SEPARATOR = /&&|\|\||;|\|/;
61
+
56
62
  const MANAGER_FIELD = /^(npm|pnpm|yarn|bun)@(\d+)/;
57
63
 
58
64
  const ENGINE_MANAGERS: readonly string[] = [
@@ -394,6 +400,18 @@ const frameworksOf = (
394
400
  return declared.length > 0 ? declared : specs.filter((spec) => spec.modules.some((name) => imports.has(name)));
395
401
  };
396
402
 
403
+ const runsWindowsCommand = (command: string | null) => {
404
+ if (command === null) {
405
+ return false;
406
+ }
407
+
408
+ return command.split(COMMAND_SEPARATOR).some((segment) => {
409
+ const program = segment.trim().split(/\s+/)[0] ?? "";
410
+
411
+ return WINDOWS_COMMANDS.has(program.toLowerCase());
412
+ });
413
+ };
414
+
397
415
  const procfileCommand = (text: string | null): DetectedCommand | null => {
398
416
  if (text === null) {
399
417
  return null;
@@ -1094,6 +1112,18 @@ export const detect = (input: DetectInput): DetectResult => {
1094
1112
  warnings.add(BotInspectWarning.NoStart);
1095
1113
  }
1096
1114
 
1115
+ const buildScript = recordOf(packageJson?.scripts)[DETECT_TABLE.node.buildScript];
1116
+
1117
+ const commands = [
1118
+ start?.command ?? null,
1119
+ build?.command ?? null,
1120
+ build?.source === DETECT_TABLE.node.manifest && typeof buildScript === "string" ? buildScript : null,
1121
+ ];
1122
+
1123
+ if (commands.some(runsWindowsCommand)) {
1124
+ warnings.add(BotInspectWarning.WindowsCommand);
1125
+ }
1126
+
1097
1127
  committedWarnings(paths, warnings);
1098
1128
 
1099
1129
  const env = envCollector(warnings);
@@ -57,6 +57,14 @@ describe("isSecretEnvFile", () => {
57
57
  name: ".env.template",
58
58
  secret: false,
59
59
  },
60
+ {
61
+ name: ".env-example",
62
+ secret: false,
63
+ },
64
+ {
65
+ name: ".env-sample",
66
+ secret: false,
67
+ },
60
68
  {
61
69
  name: ".env.dist",
62
70
  secret: false,
@@ -4,11 +4,13 @@ export const ENV_EXAMPLE_FILES: readonly string[] = [
4
4
  ".env.example",
5
5
  ".env.sample",
6
6
  ".env.template",
7
+ ".env-example",
8
+ ".env-sample",
9
+ ".env.dist",
7
10
  ];
8
11
 
9
12
  const ENV_TEMPLATE_FILES: ReadonlySet<string> = new Set([
10
13
  ...ENV_EXAMPLE_FILES,
11
- ".env.dist",
12
14
  ".env.defaults",
13
15
  ]);
14
16
 
@@ -5,6 +5,7 @@ import { parseStatus, probeMinecraftPing } from "./minecraftPing";
5
5
  const HOST = "127.0.0.1";
6
6
 
7
7
  const CLOSED_PORT = 59_441;
8
+ const CLOSED_HOST = "::1";
8
9
 
9
10
  const varInt = (value: number) => {
10
11
  const bytes: number[] = [];
@@ -294,7 +295,7 @@ describe("probeMinecraftPing", () => {
294
295
  it("rejects as unreachable when nothing listens on the port", async () => {
295
296
  const error = await probeMinecraftPing(
296
297
  {
297
- address: HOST,
298
+ address: CLOSED_HOST,
298
299
  port: CLOSED_PORT,
299
300
  },
300
301
  2000,
@@ -19,6 +19,7 @@ import { createRconWriter, type RconWriter } from "./rconWriter";
19
19
  const HOST = "127.0.0.1";
20
20
 
21
21
  const CLOSED_PORT = 59_433;
22
+ const CLOSED_HOST = "::1";
22
23
 
23
24
  const PASSWORD = "kX7-forbidden-in-errors";
24
25
 
@@ -1005,7 +1006,7 @@ describe("RconClient failures", () => {
1005
1006
 
1006
1007
  it("reports a port nothing listens on", async () => {
1007
1008
  const client = new RconClient({
1008
- host: HOST,
1009
+ host: CLOSED_HOST,
1009
1010
  port: CLOSED_PORT,
1010
1011
  password: PASSWORD,
1011
1012
  timeoutMs: 1000,
@@ -1475,7 +1476,7 @@ describe("RconClient secrecy", () => {
1475
1476
  });
1476
1477
 
1477
1478
  const unreachable = new RconClient({
1478
- host: HOST,
1479
+ host: CLOSED_HOST,
1479
1480
  port: CLOSED_PORT,
1480
1481
  password: PASSWORD,
1481
1482
  timeoutMs: 500,