@serverkgg/bridge 0.1.9 → 0.1.11
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 +1 -1
- package/src/bot/botCauses.test.ts +137 -0
- package/src/bot/botCauses.ts +13 -4
- package/src/bot/botEnvironment.test.ts +218 -1
- package/src/bot/botEnvironment.ts +85 -3
- package/src/bot/botVariables.test.ts +32 -0
- package/src/bot/botVariables.ts +9 -0
- package/src/bot/detect/botInspectWarning.ts +1 -0
- package/src/bot/detect/corpus/anonymousx1025-anonxmusic.json +3 -2
- package/src/bot/detect/corpus/bot-base-telegram-bot-template.json +3 -1
- package/src/bot/detect/corpus/nezdemkovski-telegram-bot-boilerplate.json +3 -1
- package/src/bot/detect/corpus/serverk-discord-node.json +3 -2
- package/src/bot/detect/corpus/serverk-discord-python.json +3 -2
- package/src/bot/detect/corpus/serverk-telegram-node.json +3 -2
- package/src/bot/detect/corpus/serverk-telegram-python.json +3 -2
- package/src/bot/detect/detect.test.ts +307 -0
- package/src/bot/detect/detect.ts +39 -14
- package/src/bot/detect/detectTable.ts +4 -0
- package/src/bot/detect/detectedVersion.test.ts +160 -0
- package/src/bot/detect/detectedVersion.ts +55 -0
- package/src/bot/detect/envExample.test.ts +4 -0
- package/src/bot/detect/envExample.ts +1 -0
- package/src/bot/detect/envScan.test.ts +110 -0
- package/src/bot/detect/envScan.ts +7 -3
- package/src/bot/detect/keys.test.ts +50 -0
- package/src/bot/detect/keys.ts +45 -1
- package/src/cli/validate.test.ts +33 -3
- package/src/cli/validate.ts +13 -5
- package/src/manifest/manifest.test.ts +229 -0
- package/src/manifest/manifest.ts +109 -1
- package/src/protocol/bridgeCrash.ts +10 -1
- package/src/protocol/bridgeEvents.ts +2 -0
- package/src/runtime/bridgeDriver.test.ts +24 -0
- package/src/runtime/bridgeDriver.ts +6 -2
- package/src/runtime/bridgeEvents.test.ts +65 -1
package/package.json
CHANGED
|
@@ -25,6 +25,26 @@ const CASES: readonly {
|
|
|
25
25
|
line: "GrammyError: Call to 'getMe' failed! (401: Unauthorized)",
|
|
26
26
|
cause: BridgeCrashCause.TokenInvalid,
|
|
27
27
|
},
|
|
28
|
+
{
|
|
29
|
+
line: "TelegramError: 401: Unauthorized",
|
|
30
|
+
cause: BridgeCrashCause.TokenInvalid,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
line: 'error: [polling_error] {"code":"ETELEGRAM","message":"ETELEGRAM: 401 Unauthorized"}',
|
|
34
|
+
cause: BridgeCrashCause.TokenInvalid,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
line: "TelegramApiError: 401: Unauthorized",
|
|
38
|
+
cause: BridgeCrashCause.TokenInvalid,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
line: "DiscordAPIError[0]: 401: Unauthorized",
|
|
42
|
+
cause: BridgeCrashCause.TokenInvalid,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
line: "httpx.HTTPStatusError: Client error '401 Unauthorized' for url 'https://api.telegram.org/bot***/getMe'",
|
|
46
|
+
cause: BridgeCrashCause.TokenInvalid,
|
|
47
|
+
},
|
|
28
48
|
{
|
|
29
49
|
line: "telegram.error.InvalidToken: The token `123:abc` was rejected by the server.",
|
|
30
50
|
cause: BridgeCrashCause.TokenInvalid,
|
|
@@ -101,6 +121,18 @@ const CASES: readonly {
|
|
|
101
121
|
line: "discord.errors.PrivilegedIntentsRequired: Shard ID None is requesting privileged intents that have not been explicitly enabled in the developer portal.",
|
|
102
122
|
cause: BridgeCrashCause.IntentsDisallowed,
|
|
103
123
|
},
|
|
124
|
+
{
|
|
125
|
+
line: "Error: Disallowed intents specified",
|
|
126
|
+
cause: BridgeCrashCause.IntentsDisallowed,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
line: "GatewayError: Disallowed intents specified. Make sure any privileged intents you're trying to access have been enabled in the developer portal.",
|
|
130
|
+
cause: BridgeCrashCause.IntentsDisallowed,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
line: "hikari.errors.GatewayServerClosedConnectionError: Server closed connection with code 4014 (Disallowed intent(s))",
|
|
134
|
+
cause: BridgeCrashCause.IntentsDisallowed,
|
|
135
|
+
},
|
|
104
136
|
{
|
|
105
137
|
line: "telegram.error.Conflict: Conflict: terminated by other getUpdates request; make sure that only one bot instance is running",
|
|
106
138
|
cause: BridgeCrashCause.TokenInUse,
|
|
@@ -109,6 +141,62 @@ const CASES: readonly {
|
|
|
109
141
|
line: "GrammyError: Call to 'getUpdates' failed! (409: Conflict: terminated by other getUpdates request; make sure that only one bot instance is running)",
|
|
110
142
|
cause: BridgeCrashCause.TokenInUse,
|
|
111
143
|
},
|
|
144
|
+
{
|
|
145
|
+
line: "TelegramError: 409: Conflict: terminated by other getUpdates request; make sure that only one bot instance is running",
|
|
146
|
+
cause: BridgeCrashCause.TokenInUse,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
line: 'error: [polling_error] {"code":"ETELEGRAM","message":"ETELEGRAM: 409 Conflict: terminated by other getUpdates request; make sure that only one bot instance is running"}',
|
|
150
|
+
cause: BridgeCrashCause.TokenInUse,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
line: "TelegramApiError: 409: Conflict: terminated by other getUpdates request; make sure that only one bot instance is running",
|
|
154
|
+
cause: BridgeCrashCause.TokenInUse,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
line: "TelegramError: 409: Conflict: terminated by setWebhook request",
|
|
158
|
+
cause: BridgeCrashCause.TokenInUse,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
line: "GrammyError: Call to 'getUpdates' failed! (409: Conflict: can't use getUpdates method while webhook is active; use deleteWebhook to delete the webhook first)",
|
|
162
|
+
cause: BridgeCrashCause.WebhookActive,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
line: "TelegramError: 409: Conflict: can't use getUpdates method while webhook is active; use deleteWebhook to delete the webhook first",
|
|
166
|
+
cause: BridgeCrashCause.WebhookActive,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
line: "telegram.error.Conflict: Conflict: can't use getUpdates method while webhook is active; use deleteWebhook to delete the webhook first",
|
|
170
|
+
cause: BridgeCrashCause.WebhookActive,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
line: "aiogram.exceptions.TelegramConflictError: Telegram server says - Conflict: can't use getUpdates method while webhook is active; use deleteWebhook to delete the webhook first",
|
|
174
|
+
cause: BridgeCrashCause.WebhookActive,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
line: "Error: listen EADDRINUSE: address already in use :::8080",
|
|
178
|
+
cause: BridgeCrashCause.PortBusy,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
line: "Error: listen EADDRINUSE: address already in use 0.0.0.0:8080",
|
|
182
|
+
cause: BridgeCrashCause.PortBusy,
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
line: "OSError: [Errno 98] Address already in use",
|
|
186
|
+
cause: BridgeCrashCause.PortBusy,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
line: "OSError: [Errno 98] error while attempting to bind on address ('0.0.0.0', 8080): address already in use",
|
|
190
|
+
cause: BridgeCrashCause.PortBusy,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
line: "ERROR: [Errno 98] error while attempting to bind on address ('0.0.0.0', 8080): address already in use",
|
|
194
|
+
cause: BridgeCrashCause.PortBusy,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
line: "OSError: [Errno 48] Address already in use",
|
|
198
|
+
cause: BridgeCrashCause.PortBusy,
|
|
199
|
+
},
|
|
112
200
|
{
|
|
113
201
|
line: "Error: Cannot find module 'discord.js'",
|
|
114
202
|
cause: BridgeCrashCause.MissingDependency,
|
|
@@ -185,6 +273,14 @@ const CASES: readonly {
|
|
|
185
273
|
line: "error code: 1015",
|
|
186
274
|
cause: BridgeCrashCause.RateLimited,
|
|
187
275
|
},
|
|
276
|
+
{
|
|
277
|
+
line: "Error 1015: You are being rate limited",
|
|
278
|
+
cause: BridgeCrashCause.RateLimited,
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
line: '"message": "You are being rate limited.",',
|
|
282
|
+
cause: BridgeCrashCause.RateLimited,
|
|
283
|
+
},
|
|
188
284
|
{
|
|
189
285
|
line: "FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory",
|
|
190
286
|
cause: BridgeCrashCause.OutOfMemory,
|
|
@@ -285,6 +381,26 @@ describe("BOT_CRASH_EVIDENCE", () => {
|
|
|
285
381
|
});
|
|
286
382
|
}
|
|
287
383
|
|
|
384
|
+
it("names an active webhook before the token in use its 409 line also matches", () => {
|
|
385
|
+
const { ring, print } = botRing();
|
|
386
|
+
|
|
387
|
+
print(
|
|
388
|
+
"GrammyError: Call to 'getUpdates' failed! (409: Conflict: can't use getUpdates method while webhook is active; use deleteWebhook to delete the webhook first)",
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
expect(ring.crash(recordOf())).toBe(BridgeCrashCause.WebhookActive);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("still names a second poller on the token as the token in use", () => {
|
|
395
|
+
const { ring, print } = botRing();
|
|
396
|
+
|
|
397
|
+
print(
|
|
398
|
+
"GrammyError: Call to 'getUpdates' failed! (409: Conflict: terminated by other getUpdates request; make sure that only one bot instance is running)",
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
expect(ring.crash(recordOf())).toBe(BridgeCrashCause.TokenInUse);
|
|
402
|
+
});
|
|
403
|
+
|
|
288
404
|
it("names the removed audioop module before any other missing module", () => {
|
|
289
405
|
const { ring, print } = botRing();
|
|
290
406
|
|
|
@@ -345,6 +461,27 @@ describe("BOT_CRASH_EVIDENCE", () => {
|
|
|
345
461
|
expect(ring.crash(recordOf())).toBe(BridgeCrashCause.Unknown);
|
|
346
462
|
});
|
|
347
463
|
|
|
464
|
+
for (const line of [
|
|
465
|
+
"AxiosError: Request failed with status code 401",
|
|
466
|
+
"AxiosError: Request failed with status code 409",
|
|
467
|
+
"requests.exceptions.HTTPError: 401 Client Error: Unauthorized for url: https://api.spotify.com/v1/me",
|
|
468
|
+
"requests.exceptions.HTTPError: 409 Client Error: Conflict for url: https://api.github.com/repos/owner/repo/merges",
|
|
469
|
+
"aiohttp.client_exceptions.ClientResponseError: 401, message='Unauthorized', url='https://api.spotify.com/v1/me'",
|
|
470
|
+
"httpx.HTTPStatusError: Client error '401 Unauthorized' for url 'https://api.spotify.com/v1/me'",
|
|
471
|
+
"httpx.HTTPStatusError: Client error '409 Conflict' for url 'https://api.github.com/repos/owner/repo/merges'",
|
|
472
|
+
"urllib.error.HTTPError: HTTP Error 401: Unauthorized",
|
|
473
|
+
"urllib.error.HTTPError: HTTP Error 409: Conflict",
|
|
474
|
+
"openai.AuthenticationError: Error code: 401 - {'error': {'message': 'Incorrect API key provided: sk-proj-****. You can find your API key at https://platform.openai.com/account/api-keys.', 'type': 'invalid_request_error', 'param': None, 'code': 'invalid_api_key'}}",
|
|
475
|
+
]) {
|
|
476
|
+
it(`leaves a refusal from an API other than the bot's platform unexplained from "${line}"`, () => {
|
|
477
|
+
const { ring, print } = botRing();
|
|
478
|
+
|
|
479
|
+
print(line);
|
|
480
|
+
|
|
481
|
+
expect(ring.crash(recordOf())).toBe(BridgeCrashCause.Unknown);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
348
485
|
it("calls exit 137 out of memory only when the record carries the flag", () => {
|
|
349
486
|
const unflagged = botRing();
|
|
350
487
|
|
package/src/bot/botCauses.ts
CHANGED
|
@@ -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|\bACCESS_TOKEN_(?:INVALID|EXPIRED)\b/,
|
|
7
|
+
/\[TokenInvalid\]|An invalid token was provided|LoginFailure: Improper token|(?:Telegram|ETELEGRAM|Call to '|\/bot[^/\s]*\/|DiscordAPIError\[).*\b401:? Unauthorized\b|\b401:? Unauthorized\b.*(?:Telegram|ETELEGRAM|\/bot[^/\s]*\/)|\bInvalidToken\b|TelegramUnauthorizedError|\bACCESS_TOKEN_(?:INVALID|EXPIRED)\b/,
|
|
8
8
|
emit: BridgeEventName.TokenInvalid,
|
|
9
9
|
},
|
|
10
10
|
{
|
|
@@ -19,13 +19,22 @@ export const BOT_CRASH_EVIDENCE: readonly BridgeEventPattern[] = [
|
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
match:
|
|
22
|
-
/Used disallowed intents|\[DisallowedIntents\]|Privileged intent provided is not enabled or whitelisted|PrivilegedIntentsRequired|close code:? 4014\b/,
|
|
22
|
+
/Used disallowed intents|\[DisallowedIntents\]|Privileged intent provided is not enabled or whitelisted|PrivilegedIntentsRequired|Disallowed intents specified|Server closed connection with code 4014\b|close code:? 4014\b/,
|
|
23
23
|
emit: BridgeEventName.IntentsDisallowed,
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
|
-
match: /
|
|
26
|
+
match: /can't use getUpdates method while webhook is active/,
|
|
27
|
+
emit: BridgeEventName.WebhookActive,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
match:
|
|
31
|
+
/terminated by other getUpdates request|(?:Telegram|ETELEGRAM|Call to '|\/bot[^/\s]*\/).*\b409:? Conflict\b|\b409:? Conflict\b.*(?:Telegram|ETELEGRAM|\/bot[^/\s]*\/)/,
|
|
27
32
|
emit: BridgeEventName.TokenInUse,
|
|
28
33
|
},
|
|
34
|
+
{
|
|
35
|
+
match: /EADDRINUSE|\[Errno 98\][^\n]*[Aa]ddress already in use|OSError: [^\n]*Address already in use/,
|
|
36
|
+
emit: BridgeEventName.PortBindFailed,
|
|
37
|
+
},
|
|
29
38
|
{
|
|
30
39
|
match:
|
|
31
40
|
/Cannot find module|ERR_MODULE_NOT_FOUND|ModuleNotFoundError: No module named|(?:davey|PyNaCl) library needed in order to use voice/,
|
|
@@ -63,6 +72,6 @@ export const BOT_CRASH_EVIDENCE: readonly BridgeEventPattern[] = [
|
|
|
63
72
|
{
|
|
64
73
|
match:
|
|
65
74
|
/FATAL ERROR: (?:Reached heap limit|Ineffective mark-compacts near heap limit) Allocation failed - JavaScript heap out of memory|^MemoryError\b/,
|
|
66
|
-
emit: BridgeEventName.
|
|
75
|
+
emit: BridgeEventName.OutOfMemory,
|
|
67
76
|
},
|
|
68
77
|
];
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import {
|
|
3
|
+
BOT_NO_EXPOSURE,
|
|
4
|
+
type BotExposure,
|
|
5
|
+
botExposureOf,
|
|
3
6
|
botProcessEnvironment,
|
|
4
7
|
customerEnvironment,
|
|
5
8
|
LAUNCHER_DEPLOYMENT_ID,
|
|
@@ -17,7 +20,17 @@ import {
|
|
|
17
20
|
platformEnvironment,
|
|
18
21
|
} from "./botEnvironment";
|
|
19
22
|
import { type BotConfig, DEFAULT_REPO_BRANCH } from "./botVariables";
|
|
20
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
BOT_PORT_1_KEY,
|
|
25
|
+
BOT_PORT_2_KEY,
|
|
26
|
+
BOT_WEB_PORT_KEY,
|
|
27
|
+
BotPlatform,
|
|
28
|
+
PORT,
|
|
29
|
+
SERVERK_PORT_1,
|
|
30
|
+
SERVERK_PORT_2,
|
|
31
|
+
SERVERK_WEB_HOST,
|
|
32
|
+
SERVERK_WEB_URL,
|
|
33
|
+
} from "./detect";
|
|
21
34
|
|
|
22
35
|
const SHA = "0123456789abcdef0123456789abcdef01234567";
|
|
23
36
|
|
|
@@ -32,9 +45,33 @@ const config = (overrides: Partial<BotConfig> = {}): BotConfig => ({
|
|
|
32
45
|
deployment: null,
|
|
33
46
|
snapshot: null,
|
|
34
47
|
gitToken: null,
|
|
48
|
+
webUrl: null,
|
|
49
|
+
...overrides,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const WEB_URL = "https://my-bot.srvk.gg";
|
|
53
|
+
|
|
54
|
+
const exposureOf = (overrides: Partial<BotExposure> = {}): BotExposure => ({
|
|
55
|
+
...BOT_NO_EXPOSURE,
|
|
35
56
|
...overrides,
|
|
36
57
|
});
|
|
37
58
|
|
|
59
|
+
const contextWith = (ports: Record<string, number>, variables: Record<string, string> = {}) => {
|
|
60
|
+
return {
|
|
61
|
+
port: (key: string) => {
|
|
62
|
+
const value = ports[key];
|
|
63
|
+
|
|
64
|
+
if (value === undefined) {
|
|
65
|
+
throw new Error(`this game declares no port named "${key}"`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return value;
|
|
69
|
+
},
|
|
70
|
+
variable: (key: string) => variables[key] ?? null,
|
|
71
|
+
secret: () => null,
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
38
75
|
describe("customerEnvironment", () => {
|
|
39
76
|
it("hands the process every vault key and nothing the platform or the runtime owns", () => {
|
|
40
77
|
expect(
|
|
@@ -152,6 +189,153 @@ describe("platformEnvironment", () => {
|
|
|
152
189
|
SERVERK_SERVER_CODE: "abc",
|
|
153
190
|
});
|
|
154
191
|
});
|
|
192
|
+
|
|
193
|
+
it("hands the bot PORT, the web address and its host while the web port is active", () => {
|
|
194
|
+
expect(
|
|
195
|
+
platformEnvironment(
|
|
196
|
+
"abc",
|
|
197
|
+
null,
|
|
198
|
+
exposureOf({
|
|
199
|
+
web: 8080,
|
|
200
|
+
webUrl: WEB_URL,
|
|
201
|
+
}),
|
|
202
|
+
),
|
|
203
|
+
).toEqual({
|
|
204
|
+
SERVERK_BOT: "1",
|
|
205
|
+
SERVERK_SERVER_CODE: "abc",
|
|
206
|
+
[PORT]: "8080",
|
|
207
|
+
[SERVERK_WEB_URL]: WEB_URL,
|
|
208
|
+
[SERVERK_WEB_HOST]: "my-bot.srvk.gg",
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("hands the bot no PORT and no address while the web port is off, even with an address delivered", () => {
|
|
213
|
+
expect(
|
|
214
|
+
platformEnvironment(
|
|
215
|
+
"abc",
|
|
216
|
+
null,
|
|
217
|
+
exposureOf({
|
|
218
|
+
webUrl: WEB_URL,
|
|
219
|
+
}),
|
|
220
|
+
),
|
|
221
|
+
).toEqual({
|
|
222
|
+
SERVERK_BOT: "1",
|
|
223
|
+
SERVERK_SERVER_CODE: "abc",
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("hands the bot PORT alone while the web port is active before its address is known", () => {
|
|
228
|
+
expect(
|
|
229
|
+
platformEnvironment(
|
|
230
|
+
"abc",
|
|
231
|
+
null,
|
|
232
|
+
exposureOf({
|
|
233
|
+
web: 8080,
|
|
234
|
+
}),
|
|
235
|
+
),
|
|
236
|
+
).toEqual({
|
|
237
|
+
SERVERK_BOT: "1",
|
|
238
|
+
SERVERK_SERVER_CODE: "abc",
|
|
239
|
+
[PORT]: "8080",
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("drops an address that is not a bare https host", () => {
|
|
244
|
+
for (const webUrl of [
|
|
245
|
+
"http://my-bot.srvk.gg",
|
|
246
|
+
"https://my-bot.srvk.gg/",
|
|
247
|
+
"https://my-bot.srvk.gg:8443",
|
|
248
|
+
"https://My-Bot.srvk.gg",
|
|
249
|
+
]) {
|
|
250
|
+
expect(
|
|
251
|
+
platformEnvironment(
|
|
252
|
+
"abc",
|
|
253
|
+
null,
|
|
254
|
+
exposureOf({
|
|
255
|
+
web: 8080,
|
|
256
|
+
webUrl,
|
|
257
|
+
}),
|
|
258
|
+
),
|
|
259
|
+
webUrl,
|
|
260
|
+
).toEqual({
|
|
261
|
+
SERVERK_BOT: "1",
|
|
262
|
+
SERVERK_SERVER_CODE: "abc",
|
|
263
|
+
[PORT]: "8080",
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("hands the bot each raw port only while it is active", () => {
|
|
269
|
+
expect(
|
|
270
|
+
platformEnvironment(
|
|
271
|
+
"abc",
|
|
272
|
+
null,
|
|
273
|
+
exposureOf({
|
|
274
|
+
port1: 29_004,
|
|
275
|
+
port2: 30_017,
|
|
276
|
+
}),
|
|
277
|
+
),
|
|
278
|
+
).toEqual({
|
|
279
|
+
SERVERK_BOT: "1",
|
|
280
|
+
SERVERK_SERVER_CODE: "abc",
|
|
281
|
+
[SERVERK_PORT_1]: "29004",
|
|
282
|
+
[SERVERK_PORT_2]: "30017",
|
|
283
|
+
});
|
|
284
|
+
expect(
|
|
285
|
+
platformEnvironment(
|
|
286
|
+
"abc",
|
|
287
|
+
null,
|
|
288
|
+
exposureOf({
|
|
289
|
+
port2: 30_017,
|
|
290
|
+
}),
|
|
291
|
+
),
|
|
292
|
+
).toEqual({
|
|
293
|
+
SERVERK_BOT: "1",
|
|
294
|
+
SERVERK_SERVER_CODE: "abc",
|
|
295
|
+
[SERVERK_PORT_2]: "30017",
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe("botExposureOf", () => {
|
|
301
|
+
it("reads the active ports and the validated address from the bridge context", () => {
|
|
302
|
+
expect(
|
|
303
|
+
botExposureOf(
|
|
304
|
+
contextWith(
|
|
305
|
+
{
|
|
306
|
+
[BOT_WEB_PORT_KEY]: 8080,
|
|
307
|
+
[BOT_PORT_2_KEY]: 30_017,
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
[SERVERK_WEB_URL]: WEB_URL,
|
|
311
|
+
},
|
|
312
|
+
),
|
|
313
|
+
),
|
|
314
|
+
).toEqual({
|
|
315
|
+
web: 8080,
|
|
316
|
+
webUrl: WEB_URL,
|
|
317
|
+
port1: null,
|
|
318
|
+
port2: 30_017,
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("answers no exposure for a bundle that declares no port", () => {
|
|
323
|
+
expect(botExposureOf(contextWith({}))).toEqual(BOT_NO_EXPOSURE);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("reads a raw port without the web port", () => {
|
|
327
|
+
expect(
|
|
328
|
+
botExposureOf(
|
|
329
|
+
contextWith({
|
|
330
|
+
[BOT_PORT_1_KEY]: 29_004,
|
|
331
|
+
}),
|
|
332
|
+
),
|
|
333
|
+
).toEqual(
|
|
334
|
+
exposureOf({
|
|
335
|
+
port1: 29_004,
|
|
336
|
+
}),
|
|
337
|
+
);
|
|
338
|
+
});
|
|
155
339
|
});
|
|
156
340
|
|
|
157
341
|
describe("botProcessEnvironment", () => {
|
|
@@ -186,4 +370,37 @@ describe("botProcessEnvironment", () => {
|
|
|
186
370
|
BOT_PLATFORM: BotPlatform.Telegram,
|
|
187
371
|
});
|
|
188
372
|
});
|
|
373
|
+
|
|
374
|
+
it("lets the platform's PORT win over the customer's while the web port is active", () => {
|
|
375
|
+
const customer = customerEnvironment(
|
|
376
|
+
{
|
|
377
|
+
[PORT]: "3000",
|
|
378
|
+
},
|
|
379
|
+
{},
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
expect(
|
|
383
|
+
botProcessEnvironment({
|
|
384
|
+
tool: {},
|
|
385
|
+
customer,
|
|
386
|
+
launcher: {},
|
|
387
|
+
platform: platformEnvironment(
|
|
388
|
+
"abc",
|
|
389
|
+
null,
|
|
390
|
+
exposureOf({
|
|
391
|
+
web: 8080,
|
|
392
|
+
webUrl: WEB_URL,
|
|
393
|
+
}),
|
|
394
|
+
),
|
|
395
|
+
})[PORT],
|
|
396
|
+
).toBe("8080");
|
|
397
|
+
expect(
|
|
398
|
+
botProcessEnvironment({
|
|
399
|
+
tool: {},
|
|
400
|
+
customer,
|
|
401
|
+
launcher: {},
|
|
402
|
+
platform: platformEnvironment("abc", null),
|
|
403
|
+
})[PORT],
|
|
404
|
+
).toBe("3000");
|
|
405
|
+
});
|
|
189
406
|
});
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { BridgeContext } from "../context";
|
|
2
|
+
import { type BotConfig, readBotConfig, WEB_URL_PATTERN } from "./botVariables";
|
|
2
3
|
import type { BotPlatform } from "./detect";
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
BOT_PLATFORM,
|
|
6
|
+
BOT_PORT_1_KEY,
|
|
7
|
+
BOT_PORT_2_KEY,
|
|
8
|
+
BOT_WEB_PORT_KEY,
|
|
9
|
+
isReservedBotKey,
|
|
10
|
+
PORT,
|
|
11
|
+
SERVERK_BOT,
|
|
12
|
+
SERVERK_PORT_1,
|
|
13
|
+
SERVERK_PORT_2,
|
|
14
|
+
SERVERK_SERVER_CODE,
|
|
15
|
+
SERVERK_WEB_HOST,
|
|
16
|
+
SERVERK_WEB_URL,
|
|
17
|
+
} from "./detect";
|
|
4
18
|
|
|
5
19
|
export const LAUNCHER_REPO_URL = "SERVERK_REPO_URL";
|
|
6
20
|
|
|
@@ -112,7 +126,73 @@ export const launcherEnvironment = (config: BotConfig, stateDir: string): Record
|
|
|
112
126
|
return environment;
|
|
113
127
|
};
|
|
114
128
|
|
|
115
|
-
export
|
|
129
|
+
export interface BotExposure {
|
|
130
|
+
web: number | null;
|
|
131
|
+
webUrl: string | null;
|
|
132
|
+
port1: number | null;
|
|
133
|
+
port2: number | null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const BOT_NO_EXPOSURE: BotExposure = {
|
|
137
|
+
web: null,
|
|
138
|
+
webUrl: null,
|
|
139
|
+
port1: null,
|
|
140
|
+
port2: null,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const activePortOf = (context: Pick<BridgeContext, "port">, key: string) => {
|
|
144
|
+
try {
|
|
145
|
+
return context.port(key);
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const botExposureOf = (context: Pick<BridgeContext, "port" | "variable" | "secret">): BotExposure => {
|
|
152
|
+
return {
|
|
153
|
+
web: activePortOf(context, BOT_WEB_PORT_KEY),
|
|
154
|
+
webUrl: readBotConfig(context).webUrl,
|
|
155
|
+
port1: activePortOf(context, BOT_PORT_1_KEY),
|
|
156
|
+
port2: activePortOf(context, BOT_PORT_2_KEY),
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const webEnvironment = (exposure: BotExposure): Record<string, string> => {
|
|
161
|
+
if (exposure.web === null) {
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const environment: Record<string, string> = {
|
|
166
|
+
[PORT]: String(exposure.web),
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
if (exposure.webUrl !== null && WEB_URL_PATTERN.test(exposure.webUrl)) {
|
|
170
|
+
environment[SERVERK_WEB_URL] = exposure.webUrl;
|
|
171
|
+
environment[SERVERK_WEB_HOST] = new URL(exposure.webUrl).hostname;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return environment;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const rawPortEnvironment = (exposure: BotExposure): Record<string, string> => {
|
|
178
|
+
const environment: Record<string, string> = {};
|
|
179
|
+
|
|
180
|
+
if (exposure.port1 !== null) {
|
|
181
|
+
environment[SERVERK_PORT_1] = String(exposure.port1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (exposure.port2 !== null) {
|
|
185
|
+
environment[SERVERK_PORT_2] = String(exposure.port2);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return environment;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const platformEnvironment = (
|
|
192
|
+
code: string,
|
|
193
|
+
platform: BotPlatform | null,
|
|
194
|
+
exposure: BotExposure = BOT_NO_EXPOSURE,
|
|
195
|
+
): Record<string, string> => {
|
|
116
196
|
return {
|
|
117
197
|
[SERVERK_BOT]: "1",
|
|
118
198
|
[SERVERK_SERVER_CODE]: code,
|
|
@@ -121,6 +201,8 @@ export const platformEnvironment = (code: string, platform: BotPlatform | null):
|
|
|
121
201
|
[BOT_PLATFORM]: platform,
|
|
122
202
|
}
|
|
123
203
|
: {}),
|
|
204
|
+
...webEnvironment(exposure),
|
|
205
|
+
...rawPortEnvironment(exposure),
|
|
124
206
|
};
|
|
125
207
|
};
|
|
126
208
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
ROOT_DIRECTORY_LENGTH_LIMIT,
|
|
10
10
|
readBotConfig,
|
|
11
11
|
SNAPSHOT_ID_PATTERN,
|
|
12
|
+
WEB_URL_PATTERN,
|
|
12
13
|
} from "./botVariables";
|
|
13
14
|
import {
|
|
14
15
|
BOT_PLATFORM,
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
REPO_REF,
|
|
21
22
|
REPO_URL,
|
|
22
23
|
ROOT_DIRECTORY,
|
|
24
|
+
SERVERK_WEB_URL,
|
|
23
25
|
SNAPSHOT_ID,
|
|
24
26
|
START_COMMAND,
|
|
25
27
|
} from "./detect";
|
|
@@ -49,6 +51,7 @@ describe("readBotConfig", () => {
|
|
|
49
51
|
[DEPLOYMENT_ID]: "dep_a1b2c3d4",
|
|
50
52
|
[SNAPSHOT_ID]: "snap_1",
|
|
51
53
|
[GIT_TOKEN]: "ignored-in-variables",
|
|
54
|
+
[SERVERK_WEB_URL]: " https://my-bot.srvk.gg ",
|
|
52
55
|
},
|
|
53
56
|
{
|
|
54
57
|
[GIT_TOKEN]: "ghs_installation",
|
|
@@ -66,6 +69,7 @@ describe("readBotConfig", () => {
|
|
|
66
69
|
deployment: "dep_a1b2c3d4",
|
|
67
70
|
snapshot: "snap_1",
|
|
68
71
|
gitToken: "ghs_installation",
|
|
72
|
+
webUrl: "https://my-bot.srvk.gg",
|
|
69
73
|
});
|
|
70
74
|
});
|
|
71
75
|
|
|
@@ -88,8 +92,29 @@ describe("readBotConfig", () => {
|
|
|
88
92
|
deployment: null,
|
|
89
93
|
snapshot: null,
|
|
90
94
|
gitToken: null,
|
|
95
|
+
webUrl: null,
|
|
91
96
|
});
|
|
92
97
|
});
|
|
98
|
+
|
|
99
|
+
it("drops a web address that is not a bare https host", () => {
|
|
100
|
+
for (const value of [
|
|
101
|
+
"http://my-bot.srvk.gg",
|
|
102
|
+
"https://my-bot.srvk.gg/webhook",
|
|
103
|
+
"https://my-bot.srvk.gg:443",
|
|
104
|
+
"https://user@my-bot.srvk.gg",
|
|
105
|
+
"https://My-Bot.srvk.gg",
|
|
106
|
+
"https://",
|
|
107
|
+
]) {
|
|
108
|
+
expect(
|
|
109
|
+
readBotConfig(
|
|
110
|
+
contextWith({
|
|
111
|
+
[SERVERK_WEB_URL]: value,
|
|
112
|
+
}),
|
|
113
|
+
).webUrl,
|
|
114
|
+
value,
|
|
115
|
+
).toBeNull();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
93
118
|
});
|
|
94
119
|
|
|
95
120
|
describe("the patterns", () => {
|
|
@@ -145,6 +170,13 @@ describe("the patterns", () => {
|
|
|
145
170
|
}
|
|
146
171
|
});
|
|
147
172
|
|
|
173
|
+
it("take a web address only as https and a lowercase host", () => {
|
|
174
|
+
expect(WEB_URL_PATTERN.test("https://my-bot.srvk.gg")).toBe(true);
|
|
175
|
+
expect(WEB_URL_PATTERN.test("https://abc123.srvk.localhost")).toBe(true);
|
|
176
|
+
expect(WEB_URL_PATTERN.test("https://my-bot.srvk.gg/")).toBe(false);
|
|
177
|
+
expect(WEB_URL_PATTERN.test("http://my-bot.srvk.gg")).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
148
180
|
it("accept https repositories and git branch names", () => {
|
|
149
181
|
expect(REPO_URL_PATTERN.test("https://github.com/you/bot.git")).toBe(true);
|
|
150
182
|
expect(REPO_URL_PATTERN.test("https://user:pass@github.com/you/bot")).toBe(false);
|
package/src/bot/botVariables.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
REPO_REF,
|
|
11
11
|
REPO_URL,
|
|
12
12
|
ROOT_DIRECTORY,
|
|
13
|
+
SERVERK_WEB_URL,
|
|
13
14
|
SNAPSHOT_ID,
|
|
14
15
|
START_COMMAND,
|
|
15
16
|
} from "./detect";
|
|
@@ -30,6 +31,8 @@ export const ROOT_DIRECTORY_PATTERN =
|
|
|
30
31
|
|
|
31
32
|
export const ROOT_DIRECTORY_LENGTH_LIMIT = 255;
|
|
32
33
|
|
|
34
|
+
export const WEB_URL_PATTERN = /^https:\/\/[a-z0-9.-]+$/;
|
|
35
|
+
|
|
33
36
|
export const DEFAULT_REPO_BRANCH = "main";
|
|
34
37
|
|
|
35
38
|
export const isRootDirectory = (value: string) => {
|
|
@@ -47,6 +50,7 @@ export interface BotConfig {
|
|
|
47
50
|
deployment: string | null;
|
|
48
51
|
snapshot: string | null;
|
|
49
52
|
gitToken: string | null;
|
|
53
|
+
webUrl: string | null;
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
const present = (value: string | null) => {
|
|
@@ -55,6 +59,10 @@ const present = (value: string | null) => {
|
|
|
55
59
|
return trimmed.length === 0 ? null : trimmed;
|
|
56
60
|
};
|
|
57
61
|
|
|
62
|
+
const webUrlOf = (value: string | null) => {
|
|
63
|
+
return value !== null && WEB_URL_PATTERN.test(value) ? value : null;
|
|
64
|
+
};
|
|
65
|
+
|
|
58
66
|
export const readBotConfig = (context: Pick<BridgeContext, "variable" | "secret">): BotConfig => {
|
|
59
67
|
const platform = present(context.variable(BOT_PLATFORM));
|
|
60
68
|
|
|
@@ -69,5 +77,6 @@ export const readBotConfig = (context: Pick<BridgeContext, "variable" | "secret"
|
|
|
69
77
|
deployment: present(context.variable(DEPLOYMENT_ID)),
|
|
70
78
|
snapshot: present(context.variable(SNAPSHOT_ID)),
|
|
71
79
|
gitToken: present(context.secret(GIT_TOKEN)),
|
|
80
|
+
webUrl: webUrlOf(present(context.variable(SERVERK_WEB_URL))),
|
|
72
81
|
};
|
|
73
82
|
};
|