@vellumai/vellum-gateway 0.3.16 → 0.3.18
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/__tests__/browser-relay-websocket.test.ts +257 -0
- package/src/__tests__/load-guards.test.ts +3 -0
- package/src/__tests__/oauth-callback.test.ts +3 -0
- package/src/__tests__/resolve-assistant.test.ts +3 -0
- package/src/__tests__/runtime-client.test.ts +3 -0
- package/src/__tests__/runtime-proxy-auth.test.ts +3 -0
- package/src/__tests__/runtime-proxy.test.ts +3 -0
- package/src/__tests__/sms-ingress-guard.test.ts +3 -0
- package/src/__tests__/telegram-api-redaction.test.ts +3 -0
- package/src/__tests__/telegram-deliver-auth.test.ts +3 -0
- package/src/__tests__/telegram-reconcile-route.test.ts +3 -0
- package/src/__tests__/telegram-send-attachments.test.ts +3 -0
- package/src/__tests__/telegram-webhook-handler.test.ts +3 -0
- package/src/__tests__/telegram-webhook-manager.test.ts +3 -0
- package/src/__tests__/twilio-relay-websocket.test.ts +3 -0
- package/src/__tests__/twilio-webhooks.test.ts +3 -0
- package/src/http/routes/browser-relay-websocket.ts +195 -0
- package/src/http/routes/slack-deliver.ts +1 -1
- package/src/http/routes/sms-deliver.test.ts +3 -0
- package/src/http/routes/telegram-deliver.test.ts +3 -0
- package/src/http/routes/telegram-webhook.test.ts +3 -0
- package/src/http/routes/twilio-relay-websocket.ts +1 -1
- package/src/http/routes/twilio-sms-webhook.test.ts +3 -0
- package/src/http/routes/twilio-voice-webhook.test.ts +3 -0
- package/src/http/routes/whatsapp-deliver.test.ts +3 -0
- package/src/http/routes/whatsapp-webhook.test.ts +3 -0
- package/src/index.ts +43 -1
- package/src/telegram/send.test.ts +3 -0
package/package.json
CHANGED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { describe, test, expect, mock, beforeEach, afterAll } from "bun:test";
|
|
2
|
+
import type { GatewayConfig } from "../config.js";
|
|
3
|
+
import {
|
|
4
|
+
createBrowserRelayWebsocketHandler,
|
|
5
|
+
getBrowserRelayWebsocketHandlers,
|
|
6
|
+
} from "../http/routes/browser-relay-websocket.js";
|
|
7
|
+
|
|
8
|
+
const WS_CONNECTING = WebSocket.CONNECTING; // 0
|
|
9
|
+
const WS_OPEN = WebSocket.OPEN; // 1
|
|
10
|
+
const WS_CLOSED = WebSocket.CLOSED; // 3
|
|
11
|
+
|
|
12
|
+
function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
13
|
+
const merged: GatewayConfig = {
|
|
14
|
+
telegramBotToken: "tok",
|
|
15
|
+
telegramWebhookSecret: "wh-ver",
|
|
16
|
+
telegramApiBaseUrl: "https://api.telegram.org",
|
|
17
|
+
assistantRuntimeBaseUrl: "http://localhost:7821",
|
|
18
|
+
routingEntries: [],
|
|
19
|
+
defaultAssistantId: undefined,
|
|
20
|
+
unmappedPolicy: "reject",
|
|
21
|
+
port: 7830,
|
|
22
|
+
runtimeBearerToken: undefined,
|
|
23
|
+
runtimeGatewayOriginSecret: undefined,
|
|
24
|
+
runtimeProxyEnabled: false,
|
|
25
|
+
runtimeProxyRequireAuth: true,
|
|
26
|
+
runtimeProxyBearerToken: undefined,
|
|
27
|
+
shutdownDrainMs: 5000,
|
|
28
|
+
runtimeTimeoutMs: 30000,
|
|
29
|
+
runtimeMaxRetries: 2,
|
|
30
|
+
runtimeInitialBackoffMs: 500,
|
|
31
|
+
telegramDeliverAuthBypass: false,
|
|
32
|
+
telegramInitialBackoffMs: 1000,
|
|
33
|
+
telegramMaxRetries: 3,
|
|
34
|
+
telegramTimeoutMs: 15000,
|
|
35
|
+
maxWebhookPayloadBytes: 1048576,
|
|
36
|
+
logFile: { dir: undefined, retentionDays: 30 },
|
|
37
|
+
maxAttachmentBytes: 20971520,
|
|
38
|
+
maxAttachmentConcurrency: 3,
|
|
39
|
+
twilioAuthToken: undefined,
|
|
40
|
+
twilioAccountSid: undefined,
|
|
41
|
+
twilioPhoneNumber: undefined,
|
|
42
|
+
smsDeliverAuthBypass: false,
|
|
43
|
+
ingressPublicBaseUrl: undefined,
|
|
44
|
+
gatewayInternalBaseUrl: "http://127.0.0.1:7830",
|
|
45
|
+
whatsappPhoneNumberId: undefined,
|
|
46
|
+
whatsappAccessToken: undefined,
|
|
47
|
+
whatsappAppSecret: undefined,
|
|
48
|
+
whatsappWebhookVerifyToken: undefined,
|
|
49
|
+
whatsappDeliverAuthBypass: false,
|
|
50
|
+
whatsappTimeoutMs: 15000,
|
|
51
|
+
whatsappMaxRetries: 3,
|
|
52
|
+
whatsappInitialBackoffMs: 1000,
|
|
53
|
+
slackChannelBotToken: undefined,
|
|
54
|
+
slackChannelAppToken: undefined,
|
|
55
|
+
slackDeliverAuthBypass: false,
|
|
56
|
+
trustProxy: false,
|
|
57
|
+
...overrides,
|
|
58
|
+
};
|
|
59
|
+
if (merged.runtimeGatewayOriginSecret === undefined) {
|
|
60
|
+
merged.runtimeGatewayOriginSecret = merged.runtimeBearerToken;
|
|
61
|
+
}
|
|
62
|
+
return merged;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createFakeDownstreamWs(data: Record<string, unknown> = {}) {
|
|
66
|
+
const sent: (string | Uint8Array)[] = [];
|
|
67
|
+
const closes: { code: number; reason: string }[] = [];
|
|
68
|
+
return {
|
|
69
|
+
data,
|
|
70
|
+
sent,
|
|
71
|
+
closes,
|
|
72
|
+
send: mock((msg: string | Uint8Array) => {
|
|
73
|
+
sent.push(msg);
|
|
74
|
+
}),
|
|
75
|
+
close: mock((code?: number, reason?: string) => {
|
|
76
|
+
closes.push({ code: code ?? 1000, reason: reason ?? "" });
|
|
77
|
+
}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function createFakeUpstreamWs() {
|
|
82
|
+
const listeners: Record<string, ((...args: unknown[]) => void)[]> = {};
|
|
83
|
+
const sent: unknown[] = [];
|
|
84
|
+
return {
|
|
85
|
+
readyState: WS_CONNECTING as number,
|
|
86
|
+
sent,
|
|
87
|
+
listeners,
|
|
88
|
+
addEventListener: mock((event: string, cb: (...args: unknown[]) => void) => {
|
|
89
|
+
(listeners[event] ??= []).push(cb);
|
|
90
|
+
}),
|
|
91
|
+
send: mock((msg: unknown) => {
|
|
92
|
+
sent.push(msg);
|
|
93
|
+
}),
|
|
94
|
+
close: mock(() => {}),
|
|
95
|
+
emit(event: string, detail: unknown = {}) {
|
|
96
|
+
for (const cb of listeners[event] ?? []) {
|
|
97
|
+
cb(detail);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
describe("createBrowserRelayWebsocketHandler", () => {
|
|
104
|
+
const TEST_TOKEN = "relay-token-abc123";
|
|
105
|
+
|
|
106
|
+
test("upgrades when token query parameter is valid", () => {
|
|
107
|
+
const config = makeConfig({ runtimeProxyBearerToken: TEST_TOKEN });
|
|
108
|
+
const handler = createBrowserRelayWebsocketHandler(config);
|
|
109
|
+
const req = new Request(
|
|
110
|
+
`http://localhost:7830/v1/browser-relay?token=${TEST_TOKEN}`,
|
|
111
|
+
{ headers: { upgrade: "websocket" } },
|
|
112
|
+
);
|
|
113
|
+
const fakeServer = {
|
|
114
|
+
requestIP: mock(() => ({ address: "127.0.0.1", family: "IPv4", port: 54000 })),
|
|
115
|
+
upgrade: mock(() => true),
|
|
116
|
+
} as unknown as import("bun").Server<any>;
|
|
117
|
+
const res = handler(req, fakeServer);
|
|
118
|
+
|
|
119
|
+
expect(res).toBeUndefined();
|
|
120
|
+
expect(fakeServer.upgrade).toHaveBeenCalledTimes(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("returns 401 when token is missing", () => {
|
|
124
|
+
const config = makeConfig({ runtimeProxyBearerToken: TEST_TOKEN });
|
|
125
|
+
const handler = createBrowserRelayWebsocketHandler(config);
|
|
126
|
+
const req = new Request("http://localhost:7830/v1/browser-relay", {
|
|
127
|
+
headers: { upgrade: "websocket" },
|
|
128
|
+
});
|
|
129
|
+
const fakeServer = {
|
|
130
|
+
requestIP: mock(() => ({ address: "127.0.0.1", family: "IPv4", port: 54000 })),
|
|
131
|
+
upgrade: mock(() => true),
|
|
132
|
+
} as unknown as import("bun").Server<any>;
|
|
133
|
+
const res = handler(req, fakeServer);
|
|
134
|
+
|
|
135
|
+
expect(res).toBeInstanceOf(Response);
|
|
136
|
+
expect(res!.status).toBe(401);
|
|
137
|
+
expect(fakeServer.upgrade).not.toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("allows unauthenticated upgrade when runtime proxy auth is disabled", () => {
|
|
141
|
+
const config = makeConfig({ runtimeProxyRequireAuth: false, runtimeProxyBearerToken: undefined });
|
|
142
|
+
const handler = createBrowserRelayWebsocketHandler(config);
|
|
143
|
+
const req = new Request("http://localhost:7830/v1/browser-relay", {
|
|
144
|
+
headers: { upgrade: "websocket" },
|
|
145
|
+
});
|
|
146
|
+
const fakeServer = {
|
|
147
|
+
requestIP: mock(() => ({ address: "127.0.0.1", family: "IPv4", port: 54000 })),
|
|
148
|
+
upgrade: mock(() => true),
|
|
149
|
+
} as unknown as import("bun").Server<any>;
|
|
150
|
+
const res = handler(req, fakeServer);
|
|
151
|
+
|
|
152
|
+
expect(res).toBeUndefined();
|
|
153
|
+
expect(fakeServer.upgrade).toHaveBeenCalledTimes(1);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("returns 403 when non-loopback host is requested from a public peer", () => {
|
|
157
|
+
const config = makeConfig({ runtimeProxyBearerToken: TEST_TOKEN });
|
|
158
|
+
const handler = createBrowserRelayWebsocketHandler(config);
|
|
159
|
+
const req = new Request(
|
|
160
|
+
`http://gateway.example.com:7830/v1/browser-relay?token=${TEST_TOKEN}`,
|
|
161
|
+
{ headers: { upgrade: "websocket" } },
|
|
162
|
+
);
|
|
163
|
+
const fakeServer = {
|
|
164
|
+
requestIP: mock(() => ({ address: "8.8.8.8", family: "IPv4", port: 54000 })),
|
|
165
|
+
upgrade: mock(() => true),
|
|
166
|
+
} as unknown as import("bun").Server<any>;
|
|
167
|
+
|
|
168
|
+
const res = handler(req, fakeServer);
|
|
169
|
+
|
|
170
|
+
expect(res).toBeInstanceOf(Response);
|
|
171
|
+
expect(res!.status).toBe(403);
|
|
172
|
+
expect(fakeServer.upgrade).not.toHaveBeenCalled();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("returns 403 for localhost host when peer is public (host spoof prevention)", () => {
|
|
176
|
+
const config = makeConfig({ runtimeProxyBearerToken: TEST_TOKEN });
|
|
177
|
+
const handler = createBrowserRelayWebsocketHandler(config);
|
|
178
|
+
const req = new Request(
|
|
179
|
+
`http://localhost:7830/v1/browser-relay?token=${TEST_TOKEN}`,
|
|
180
|
+
{ headers: { upgrade: "websocket" } },
|
|
181
|
+
);
|
|
182
|
+
const fakeServer = {
|
|
183
|
+
requestIP: mock(() => ({ address: "8.8.8.8", family: "IPv4", port: 54000 })),
|
|
184
|
+
upgrade: mock(() => true),
|
|
185
|
+
} as unknown as import("bun").Server<any>;
|
|
186
|
+
|
|
187
|
+
const res = handler(req, fakeServer);
|
|
188
|
+
|
|
189
|
+
expect(res).toBeInstanceOf(Response);
|
|
190
|
+
expect(res!.status).toBe(403);
|
|
191
|
+
expect(fakeServer.upgrade).not.toHaveBeenCalled();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("allows non-loopback host when peer is private network", () => {
|
|
195
|
+
const config = makeConfig({ runtimeProxyBearerToken: TEST_TOKEN });
|
|
196
|
+
const handler = createBrowserRelayWebsocketHandler(config);
|
|
197
|
+
const req = new Request(
|
|
198
|
+
`http://gateway.example.com:7830/v1/browser-relay?token=${TEST_TOKEN}`,
|
|
199
|
+
{ headers: { upgrade: "websocket" } },
|
|
200
|
+
);
|
|
201
|
+
const fakeServer = {
|
|
202
|
+
requestIP: mock(() => ({ address: "10.42.0.8", family: "IPv4", port: 54000 })),
|
|
203
|
+
upgrade: mock(() => true),
|
|
204
|
+
} as unknown as import("bun").Server<any>;
|
|
205
|
+
|
|
206
|
+
const res = handler(req, fakeServer);
|
|
207
|
+
|
|
208
|
+
expect(res).toBeUndefined();
|
|
209
|
+
expect(fakeServer.upgrade).toHaveBeenCalledTimes(1);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("getBrowserRelayWebsocketHandlers", () => {
|
|
214
|
+
const OriginalWebSocket = globalThis.WebSocket;
|
|
215
|
+
let fakeUpstream: ReturnType<typeof createFakeUpstreamWs>;
|
|
216
|
+
let handlers: ReturnType<typeof getBrowserRelayWebsocketHandlers>;
|
|
217
|
+
|
|
218
|
+
beforeEach(() => {
|
|
219
|
+
fakeUpstream = createFakeUpstreamWs();
|
|
220
|
+
const MockWS = mock(() => fakeUpstream);
|
|
221
|
+
Object.assign(MockWS, {
|
|
222
|
+
CONNECTING: WS_CONNECTING,
|
|
223
|
+
OPEN: WS_OPEN,
|
|
224
|
+
CLOSING: 2,
|
|
225
|
+
CLOSED: WS_CLOSED,
|
|
226
|
+
});
|
|
227
|
+
globalThis.WebSocket = MockWS as unknown as typeof WebSocket;
|
|
228
|
+
handlers = getBrowserRelayWebsocketHandlers();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
afterAll(() => {
|
|
232
|
+
globalThis.WebSocket = OriginalWebSocket;
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("open targets runtime browser-relay websocket and flushes buffered messages", () => {
|
|
236
|
+
const ws = createFakeDownstreamWs({
|
|
237
|
+
wsType: "browser-relay",
|
|
238
|
+
config: makeConfig({
|
|
239
|
+
assistantRuntimeBaseUrl: "http://runtime.internal:7821",
|
|
240
|
+
runtimeBearerToken: "runtime-token",
|
|
241
|
+
}),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
handlers.open(ws as never);
|
|
245
|
+
handlers.message(ws as never, "hello-before-open");
|
|
246
|
+
|
|
247
|
+
const MockWS = globalThis.WebSocket as unknown as ReturnType<typeof mock>;
|
|
248
|
+
expect(MockWS).toHaveBeenCalledWith("ws://runtime.internal:7821/v1/browser-relay?token=runtime-token");
|
|
249
|
+
|
|
250
|
+
fakeUpstream.readyState = WS_OPEN;
|
|
251
|
+
fakeUpstream.emit("open");
|
|
252
|
+
expect(fakeUpstream.sent).toEqual(["hello-before-open"]);
|
|
253
|
+
|
|
254
|
+
fakeUpstream.emit("message", { data: "runtime-message" });
|
|
255
|
+
expect(ws.sent).toEqual(["runtime-message"]);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -43,6 +43,9 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
43
43
|
whatsappTimeoutMs: 15000,
|
|
44
44
|
whatsappMaxRetries: 3,
|
|
45
45
|
whatsappInitialBackoffMs: 1000,
|
|
46
|
+
slackChannelBotToken: undefined,
|
|
47
|
+
slackChannelAppToken: undefined,
|
|
48
|
+
slackDeliverAuthBypass: false,
|
|
46
49
|
trustProxy: false,
|
|
47
50
|
...overrides,
|
|
48
51
|
};
|
|
@@ -55,6 +55,9 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
55
55
|
whatsappTimeoutMs: 15000,
|
|
56
56
|
whatsappMaxRetries: 3,
|
|
57
57
|
whatsappInitialBackoffMs: 1000,
|
|
58
|
+
slackChannelBotToken: undefined,
|
|
59
|
+
slackChannelAppToken: undefined,
|
|
60
|
+
slackDeliverAuthBypass: false,
|
|
58
61
|
trustProxy: false,
|
|
59
62
|
...overrides,
|
|
60
63
|
};
|
|
@@ -43,6 +43,9 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
43
43
|
whatsappTimeoutMs: 15000,
|
|
44
44
|
whatsappMaxRetries: 3,
|
|
45
45
|
whatsappInitialBackoffMs: 1000,
|
|
46
|
+
slackChannelBotToken: undefined,
|
|
47
|
+
slackChannelAppToken: undefined,
|
|
48
|
+
slackDeliverAuthBypass: false,
|
|
46
49
|
trustProxy: false,
|
|
47
50
|
...overrides,
|
|
48
51
|
};
|
|
@@ -58,6 +58,9 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
58
58
|
whatsappTimeoutMs: 15000,
|
|
59
59
|
whatsappMaxRetries: 3,
|
|
60
60
|
whatsappInitialBackoffMs: 1000,
|
|
61
|
+
slackChannelBotToken: undefined,
|
|
62
|
+
slackChannelAppToken: undefined,
|
|
63
|
+
slackDeliverAuthBypass: false,
|
|
61
64
|
trustProxy: false,
|
|
62
65
|
...overrides,
|
|
63
66
|
};
|
|
@@ -53,6 +53,9 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
53
53
|
whatsappTimeoutMs: 15000,
|
|
54
54
|
whatsappMaxRetries: 3,
|
|
55
55
|
whatsappInitialBackoffMs: 1000,
|
|
56
|
+
slackChannelBotToken: undefined,
|
|
57
|
+
slackChannelAppToken: undefined,
|
|
58
|
+
slackDeliverAuthBypass: false,
|
|
56
59
|
trustProxy: false,
|
|
57
60
|
...overrides,
|
|
58
61
|
};
|
|
@@ -51,6 +51,9 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
51
51
|
whatsappTimeoutMs: 15000,
|
|
52
52
|
whatsappMaxRetries: 3,
|
|
53
53
|
whatsappInitialBackoffMs: 1000,
|
|
54
|
+
slackChannelBotToken: undefined,
|
|
55
|
+
slackChannelAppToken: undefined,
|
|
56
|
+
slackDeliverAuthBypass: false,
|
|
54
57
|
trustProxy: false,
|
|
55
58
|
...overrides,
|
|
56
59
|
};
|
|
@@ -57,6 +57,9 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
57
57
|
whatsappTimeoutMs: 15000,
|
|
58
58
|
whatsappMaxRetries: 3,
|
|
59
59
|
whatsappInitialBackoffMs: 1000,
|
|
60
|
+
slackChannelBotToken: undefined,
|
|
61
|
+
slackChannelAppToken: undefined,
|
|
62
|
+
slackDeliverAuthBypass: false,
|
|
60
63
|
trustProxy: false,
|
|
61
64
|
...overrides,
|
|
62
65
|
};
|
|
@@ -51,6 +51,9 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
51
51
|
whatsappTimeoutMs: 15000,
|
|
52
52
|
whatsappMaxRetries: 3,
|
|
53
53
|
whatsappInitialBackoffMs: 1000,
|
|
54
|
+
slackChannelBotToken: undefined,
|
|
55
|
+
slackChannelAppToken: undefined,
|
|
56
|
+
slackDeliverAuthBypass: false,
|
|
54
57
|
trustProxy: false,
|
|
55
58
|
...overrides,
|
|
56
59
|
};
|
|
@@ -53,6 +53,9 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
53
53
|
whatsappTimeoutMs: 15000,
|
|
54
54
|
whatsappMaxRetries: 3,
|
|
55
55
|
whatsappInitialBackoffMs: 1000,
|
|
56
|
+
slackChannelBotToken: undefined,
|
|
57
|
+
slackChannelAppToken: undefined,
|
|
58
|
+
slackDeliverAuthBypass: false,
|
|
56
59
|
trustProxy: false,
|
|
57
60
|
...overrides,
|
|
58
61
|
};
|
|
@@ -58,6 +58,9 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
58
58
|
whatsappTimeoutMs: 15000,
|
|
59
59
|
whatsappMaxRetries: 3,
|
|
60
60
|
whatsappInitialBackoffMs: 1000,
|
|
61
|
+
slackChannelBotToken: undefined,
|
|
62
|
+
slackChannelAppToken: undefined,
|
|
63
|
+
slackDeliverAuthBypass: false,
|
|
61
64
|
trustProxy: false,
|
|
62
65
|
...overrides,
|
|
63
66
|
};
|
|
@@ -52,6 +52,9 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
52
52
|
whatsappTimeoutMs: 15000,
|
|
53
53
|
whatsappMaxRetries: 3,
|
|
54
54
|
whatsappInitialBackoffMs: 1000,
|
|
55
|
+
slackChannelBotToken: undefined,
|
|
56
|
+
slackChannelAppToken: undefined,
|
|
57
|
+
slackDeliverAuthBypass: false,
|
|
55
58
|
trustProxy: false,
|
|
56
59
|
...overrides,
|
|
57
60
|
};
|
|
@@ -51,6 +51,9 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
51
51
|
whatsappTimeoutMs: 15000,
|
|
52
52
|
whatsappMaxRetries: 3,
|
|
53
53
|
whatsappInitialBackoffMs: 1000,
|
|
54
|
+
slackChannelBotToken: undefined,
|
|
55
|
+
slackChannelAppToken: undefined,
|
|
56
|
+
slackDeliverAuthBypass: false,
|
|
54
57
|
trustProxy: false,
|
|
55
58
|
...overrides,
|
|
56
59
|
};
|
|
@@ -51,6 +51,9 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
51
51
|
whatsappTimeoutMs: 15000,
|
|
52
52
|
whatsappMaxRetries: 3,
|
|
53
53
|
whatsappInitialBackoffMs: 1000,
|
|
54
|
+
slackChannelBotToken: undefined,
|
|
55
|
+
slackChannelAppToken: undefined,
|
|
56
|
+
slackDeliverAuthBypass: false,
|
|
54
57
|
trustProxy: false,
|
|
55
58
|
...overrides,
|
|
56
59
|
};
|
|
@@ -58,6 +58,9 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
58
58
|
whatsappTimeoutMs: 15000,
|
|
59
59
|
whatsappMaxRetries: 3,
|
|
60
60
|
whatsappInitialBackoffMs: 1000,
|
|
61
|
+
slackChannelBotToken: undefined,
|
|
62
|
+
slackChannelAppToken: undefined,
|
|
63
|
+
slackDeliverAuthBypass: false,
|
|
61
64
|
trustProxy: false,
|
|
62
65
|
...overrides,
|
|
63
66
|
};
|
|
@@ -56,6 +56,9 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
56
56
|
whatsappTimeoutMs: 15000,
|
|
57
57
|
whatsappMaxRetries: 3,
|
|
58
58
|
whatsappInitialBackoffMs: 1000,
|
|
59
|
+
slackChannelBotToken: undefined,
|
|
60
|
+
slackChannelAppToken: undefined,
|
|
61
|
+
slackDeliverAuthBypass: false,
|
|
59
62
|
trustProxy: false,
|
|
60
63
|
...overrides,
|
|
61
64
|
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { GatewayConfig } from "../../config.js";
|
|
2
|
+
import { getLogger } from "../../logger.js";
|
|
3
|
+
import { validateBearerToken } from "../auth/bearer.js";
|
|
4
|
+
|
|
5
|
+
const log = getLogger("browser-relay-ws");
|
|
6
|
+
|
|
7
|
+
// Cap buffered messages to prevent unbounded memory growth if upstream stalls
|
|
8
|
+
const MAX_PENDING_MESSAGES = 100;
|
|
9
|
+
|
|
10
|
+
function isPrivateAddress(addr: string): boolean {
|
|
11
|
+
const v4Mapped = addr.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
|
|
12
|
+
const normalized = v4Mapped ? v4Mapped[1] : addr;
|
|
13
|
+
|
|
14
|
+
if (normalized.includes(".")) {
|
|
15
|
+
const parts = normalized.split(".").map(Number);
|
|
16
|
+
if (parts.length !== 4 || parts.some((p) => Number.isNaN(p) || p < 0 || p > 255)) return false;
|
|
17
|
+
|
|
18
|
+
if (parts[0] === 127) return true;
|
|
19
|
+
if (parts[0] === 10) return true;
|
|
20
|
+
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
|
|
21
|
+
if (parts[0] === 192 && parts[1] === 168) return true;
|
|
22
|
+
if (parts[0] === 169 && parts[1] === 254) return true;
|
|
23
|
+
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const lower = normalized.toLowerCase();
|
|
28
|
+
if (lower === "::1") return true;
|
|
29
|
+
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
|
30
|
+
if (lower.startsWith("fe80")) return true;
|
|
31
|
+
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isPrivateNetworkPeer(server: import("bun").Server<unknown>, req: Request): boolean {
|
|
36
|
+
const ip = server.requestIP(req);
|
|
37
|
+
if (!ip) return false;
|
|
38
|
+
return isPrivateAddress(ip.address);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type BrowserRelaySocketData = {
|
|
42
|
+
wsType: "browser-relay";
|
|
43
|
+
config: GatewayConfig;
|
|
44
|
+
clientToken?: string;
|
|
45
|
+
upstream?: WebSocket;
|
|
46
|
+
pendingMessages?: (string | ArrayBuffer | Uint8Array)[];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a WebSocket upgrade handler that proxies browser-relay frames between
|
|
51
|
+
* the local Chrome extension and the runtime's /v1/browser-relay endpoint.
|
|
52
|
+
*/
|
|
53
|
+
export function createBrowserRelayWebsocketHandler(config: GatewayConfig) {
|
|
54
|
+
return function handleUpgrade(
|
|
55
|
+
req: Request,
|
|
56
|
+
server: import("bun").Server<unknown>,
|
|
57
|
+
): Response | undefined {
|
|
58
|
+
if (req.headers.get("upgrade")?.toLowerCase() !== "websocket") {
|
|
59
|
+
return new Response("Upgrade Required", { status: 426 });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const url = new URL(req.url);
|
|
63
|
+
|
|
64
|
+
// Trust actual peer IP, not request host headers, for local/private gating.
|
|
65
|
+
if (!isPrivateNetworkPeer(server, req)) {
|
|
66
|
+
return new Response("Browser relay only accepts connections from localhost", { status: 403 });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const authResponse = checkBrowserRelayAuth(req, url, config);
|
|
70
|
+
if (authResponse) return authResponse;
|
|
71
|
+
|
|
72
|
+
const upgraded = server.upgrade(req, {
|
|
73
|
+
data: {
|
|
74
|
+
wsType: "browser-relay",
|
|
75
|
+
config,
|
|
76
|
+
clientToken: url.searchParams.get("token") ?? undefined,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!upgraded) {
|
|
81
|
+
return new Response("WebSocket upgrade failed", { status: 500 });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Return undefined to indicate upgrade was handled
|
|
85
|
+
return undefined;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function checkBrowserRelayAuth(
|
|
90
|
+
req: Request,
|
|
91
|
+
url: URL,
|
|
92
|
+
config: GatewayConfig,
|
|
93
|
+
): Response | null {
|
|
94
|
+
if (!config.runtimeProxyRequireAuth) return null;
|
|
95
|
+
|
|
96
|
+
if (!config.runtimeProxyBearerToken) {
|
|
97
|
+
log.error("Browser relay WS: no bearer token configured — rejecting (fail-closed)");
|
|
98
|
+
return new Response("Service not configured: bearer token required", { status: 503 });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const authHeader = req.headers.get("authorization");
|
|
102
|
+
const queryToken = url.searchParams.get("token");
|
|
103
|
+
const tokenSource = authHeader ?? (queryToken ? `Bearer ${queryToken}` : null);
|
|
104
|
+
|
|
105
|
+
const result = validateBearerToken(tokenSource, config.runtimeProxyBearerToken);
|
|
106
|
+
if (!result.authorized) {
|
|
107
|
+
log.warn({ reason: result.reason }, "Browser relay WS: authentication failed");
|
|
108
|
+
return new Response("Unauthorized", { status: 401 });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* WebSocket handler config for Bun.serve() that proxies frames to runtime.
|
|
116
|
+
*/
|
|
117
|
+
export function getBrowserRelayWebsocketHandlers() {
|
|
118
|
+
return {
|
|
119
|
+
open(ws: import("bun").ServerWebSocket<BrowserRelaySocketData>) {
|
|
120
|
+
const { config } = ws.data;
|
|
121
|
+
|
|
122
|
+
// Initialize message buffer for frames arriving before upstream connects
|
|
123
|
+
ws.data.pendingMessages = [];
|
|
124
|
+
|
|
125
|
+
const runtimeBase = config.assistantRuntimeBaseUrl.replace(/^http/, "ws");
|
|
126
|
+
const upstreamToken = config.runtimeBearerToken || config.runtimeProxyBearerToken || ws.data.clientToken;
|
|
127
|
+
const query = upstreamToken ? `?token=${encodeURIComponent(upstreamToken)}` : "";
|
|
128
|
+
const upstreamUrl = `${runtimeBase}/v1/browser-relay${query}`;
|
|
129
|
+
const logSafeUpstreamUrl = `${runtimeBase}/v1/browser-relay${upstreamToken ? "?token=<redacted>" : ""}`;
|
|
130
|
+
|
|
131
|
+
log.info({ upstreamUrl: logSafeUpstreamUrl }, "Opening upstream browser relay WS to runtime");
|
|
132
|
+
|
|
133
|
+
const upstream = new WebSocket(upstreamUrl);
|
|
134
|
+
ws.data.upstream = upstream;
|
|
135
|
+
|
|
136
|
+
upstream.addEventListener("open", () => {
|
|
137
|
+
log.info("Upstream browser relay WS connected");
|
|
138
|
+
const pending = ws.data.pendingMessages;
|
|
139
|
+
if (pending) {
|
|
140
|
+
for (const msg of pending) {
|
|
141
|
+
upstream.send(msg);
|
|
142
|
+
}
|
|
143
|
+
ws.data.pendingMessages = undefined;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
upstream.addEventListener("message", (event) => {
|
|
148
|
+
const data = typeof event.data === "string"
|
|
149
|
+
? event.data
|
|
150
|
+
: new Uint8Array(event.data as ArrayBuffer);
|
|
151
|
+
ws.send(data);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
upstream.addEventListener("close", (event) => {
|
|
155
|
+
log.info({ code: event.code }, "Upstream browser relay WS closed");
|
|
156
|
+
ws.close(event.code, event.reason);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
upstream.addEventListener("error", (event) => {
|
|
160
|
+
log.error({ error: event }, "Upstream browser relay WS error");
|
|
161
|
+
ws.close(1011, "Upstream error");
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
message(
|
|
166
|
+
ws: import("bun").ServerWebSocket<BrowserRelaySocketData>,
|
|
167
|
+
message: string | ArrayBuffer | Uint8Array,
|
|
168
|
+
) {
|
|
169
|
+
const upstream = ws.data.upstream;
|
|
170
|
+
if (upstream && upstream.readyState === WebSocket.OPEN) {
|
|
171
|
+
upstream.send(message);
|
|
172
|
+
} else if (ws.data.pendingMessages) {
|
|
173
|
+
if (ws.data.pendingMessages.length >= MAX_PENDING_MESSAGES) {
|
|
174
|
+
log.warn("Browser relay pending message buffer overflow — closing connection");
|
|
175
|
+
ws.close(1008, "Buffer overflow");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
ws.data.pendingMessages.push(message);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
close(
|
|
183
|
+
ws: import("bun").ServerWebSocket<BrowserRelaySocketData>,
|
|
184
|
+
code: number,
|
|
185
|
+
reason: string,
|
|
186
|
+
) {
|
|
187
|
+
const { upstream } = ws.data;
|
|
188
|
+
log.info({ code, reason }, "Browser relay downstream WS closed");
|
|
189
|
+
ws.data.pendingMessages = undefined;
|
|
190
|
+
if (upstream && (upstream.readyState === WebSocket.OPEN || upstream.readyState === WebSocket.CONNECTING)) {
|
|
191
|
+
upstream.close(code, reason);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -39,7 +39,7 @@ export function createSlackDeliverHandler(config: GatewayConfig) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
if (typeof body !== 'object' || body === null) {
|
|
42
|
-
return Response.json({
|
|
42
|
+
return Response.json({ error: "Invalid request body" }, { status: 400 });
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
if (body.attachments && Array.isArray(body.attachments) && body.attachments.length > 0) {
|
|
@@ -55,6 +55,9 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
55
55
|
whatsappTimeoutMs: 15000,
|
|
56
56
|
whatsappMaxRetries: 3,
|
|
57
57
|
whatsappInitialBackoffMs: 1000,
|
|
58
|
+
slackChannelBotToken: undefined,
|
|
59
|
+
slackChannelAppToken: undefined,
|
|
60
|
+
slackDeliverAuthBypass: false,
|
|
58
61
|
trustProxy: false,
|
|
59
62
|
...overrides,
|
|
60
63
|
};
|
|
@@ -62,6 +62,9 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
62
62
|
whatsappTimeoutMs: 15000,
|
|
63
63
|
whatsappMaxRetries: 3,
|
|
64
64
|
whatsappInitialBackoffMs: 1000,
|
|
65
|
+
slackChannelBotToken: undefined,
|
|
66
|
+
slackChannelAppToken: undefined,
|
|
67
|
+
slackDeliverAuthBypass: false,
|
|
65
68
|
trustProxy: false,
|
|
66
69
|
...overrides,
|
|
67
70
|
};
|
|
@@ -89,6 +89,9 @@ const baseConfig: GatewayConfig = {
|
|
|
89
89
|
whatsappTimeoutMs: 15000,
|
|
90
90
|
whatsappMaxRetries: 3,
|
|
91
91
|
whatsappInitialBackoffMs: 1000,
|
|
92
|
+
slackChannelBotToken: undefined,
|
|
93
|
+
slackChannelAppToken: undefined,
|
|
94
|
+
slackDeliverAuthBypass: false,
|
|
92
95
|
trustProxy: false,
|
|
93
96
|
};
|
|
94
97
|
|
|
@@ -19,7 +19,7 @@ type RelaySocketData = {
|
|
|
19
19
|
* frames between Twilio and the runtime's /v1/calls/relay endpoint.
|
|
20
20
|
*/
|
|
21
21
|
export function createTwilioRelayWebsocketHandler(config: GatewayConfig) {
|
|
22
|
-
return function handleUpgrade(req: Request, server: import("bun").Server<
|
|
22
|
+
return function handleUpgrade(req: Request, server: import("bun").Server<unknown>): Response | undefined {
|
|
23
23
|
const url = new URL(req.url);
|
|
24
24
|
const callSessionId = url.searchParams.get("callSessionId");
|
|
25
25
|
|
|
@@ -72,6 +72,9 @@ const baseConfig: GatewayConfig = {
|
|
|
72
72
|
whatsappTimeoutMs: 15000,
|
|
73
73
|
whatsappMaxRetries: 3,
|
|
74
74
|
whatsappInitialBackoffMs: 1000,
|
|
75
|
+
slackChannelBotToken: undefined,
|
|
76
|
+
slackChannelAppToken: undefined,
|
|
77
|
+
slackDeliverAuthBypass: false,
|
|
75
78
|
trustProxy: false,
|
|
76
79
|
};
|
|
77
80
|
|
|
@@ -105,6 +105,9 @@ const baseConfig: GatewayConfig = {
|
|
|
105
105
|
whatsappTimeoutMs: 15000,
|
|
106
106
|
whatsappMaxRetries: 3,
|
|
107
107
|
whatsappInitialBackoffMs: 1000,
|
|
108
|
+
slackChannelBotToken: undefined,
|
|
109
|
+
slackChannelAppToken: undefined,
|
|
110
|
+
slackDeliverAuthBypass: false,
|
|
108
111
|
trustProxy: false,
|
|
109
112
|
};
|
|
110
113
|
|
|
@@ -59,6 +59,9 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
59
59
|
whatsappTimeoutMs: 15000,
|
|
60
60
|
whatsappMaxRetries: 3,
|
|
61
61
|
whatsappInitialBackoffMs: 1000,
|
|
62
|
+
slackChannelBotToken: undefined,
|
|
63
|
+
slackChannelAppToken: undefined,
|
|
64
|
+
slackDeliverAuthBypass: false,
|
|
62
65
|
trustProxy: false,
|
|
63
66
|
...overrides,
|
|
64
67
|
};
|
|
@@ -76,6 +76,9 @@ const baseConfig: GatewayConfig = {
|
|
|
76
76
|
whatsappTimeoutMs: 15000,
|
|
77
77
|
whatsappMaxRetries: 3,
|
|
78
78
|
whatsappInitialBackoffMs: 1000,
|
|
79
|
+
slackChannelBotToken: undefined,
|
|
80
|
+
slackChannelAppToken: undefined,
|
|
81
|
+
slackDeliverAuthBypass: false,
|
|
79
82
|
trustProxy: false,
|
|
80
83
|
};
|
|
81
84
|
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,11 @@ import { ConfigFileWatcher } from "./config-file-watcher.js";
|
|
|
7
7
|
import { loadConfig, isSlackChannelConfigured, type GatewayConfig } from "./config.js";
|
|
8
8
|
import { CredentialWatcher } from "./credential-watcher.js";
|
|
9
9
|
import { createRuntimeProxyHandler } from "./http/routes/runtime-proxy.js";
|
|
10
|
+
import {
|
|
11
|
+
createBrowserRelayWebsocketHandler,
|
|
12
|
+
getBrowserRelayWebsocketHandlers,
|
|
13
|
+
type BrowserRelaySocketData,
|
|
14
|
+
} from "./http/routes/browser-relay-websocket.js";
|
|
10
15
|
import { createTelegramDeliverHandler } from "./http/routes/telegram-deliver.js";
|
|
11
16
|
import { createTelegramReconcileHandler } from "./http/routes/telegram-reconcile.js";
|
|
12
17
|
import { createTelegramWebhookHandler } from "./http/routes/telegram-webhook.js";
|
|
@@ -41,6 +46,10 @@ let draining = false;
|
|
|
41
46
|
// Shared rate limiter for auth failures and unauthenticated endpoints
|
|
42
47
|
const authRateLimiter = new AuthRateLimiter();
|
|
43
48
|
|
|
49
|
+
function isBrowserRelaySocketData(data: unknown): data is BrowserRelaySocketData {
|
|
50
|
+
return !!data && typeof data === "object" && (data as { wsType?: unknown }).wsType === "browser-relay";
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
function getClientIp(req: Request, server: ReturnType<typeof Bun.serve>, trustProxy: boolean): string {
|
|
45
54
|
if (trustProxy) {
|
|
46
55
|
const forwarded = req.headers.get("x-forwarded-for");
|
|
@@ -126,6 +135,9 @@ function main() {
|
|
|
126
135
|
const handleTwilioStatusWebhook = createTwilioStatusWebhookHandler(config);
|
|
127
136
|
const handleTwilioConnectActionWebhook = createTwilioConnectActionWebhookHandler(config);
|
|
128
137
|
const handleTwilioRelayWs = createTwilioRelayWebsocketHandler(config);
|
|
138
|
+
const handleBrowserRelayWs = createBrowserRelayWebsocketHandler(config);
|
|
139
|
+
const twilioRelayWebsocketHandlers = getRelayWebsocketHandlers();
|
|
140
|
+
const browserRelayWebsocketHandlers = getBrowserRelayWebsocketHandlers();
|
|
129
141
|
const { handler: handleTwilioSmsWebhook, dedupCache: smsDedupCache } = createTwilioSmsWebhookHandler(config);
|
|
130
142
|
const handleSmsDeliver = createSmsDeliverHandler(config);
|
|
131
143
|
const { handler: handleWhatsAppWebhook, dedupCache: whatsappDedupCache } = createWhatsAppWebhookHandler(config);
|
|
@@ -140,7 +152,29 @@ function main() {
|
|
|
140
152
|
|
|
141
153
|
const server = Bun.serve({
|
|
142
154
|
port: config.port,
|
|
143
|
-
websocket:
|
|
155
|
+
websocket: {
|
|
156
|
+
open(ws) {
|
|
157
|
+
if (isBrowserRelaySocketData(ws.data)) {
|
|
158
|
+
browserRelayWebsocketHandlers.open(ws as never);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
twilioRelayWebsocketHandlers.open(ws as never);
|
|
162
|
+
},
|
|
163
|
+
message(ws, message) {
|
|
164
|
+
if (isBrowserRelaySocketData(ws.data)) {
|
|
165
|
+
browserRelayWebsocketHandlers.message(ws as never, message);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
twilioRelayWebsocketHandlers.message(ws as never, message);
|
|
169
|
+
},
|
|
170
|
+
close(ws, code, reason) {
|
|
171
|
+
if (isBrowserRelaySocketData(ws.data)) {
|
|
172
|
+
browserRelayWebsocketHandlers.close(ws as never, code, reason);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
twilioRelayWebsocketHandlers.close(ws as never, code, reason);
|
|
176
|
+
},
|
|
177
|
+
},
|
|
144
178
|
error(err) {
|
|
145
179
|
if (err instanceof CircuitBreakerOpenError) {
|
|
146
180
|
return Response.json(
|
|
@@ -183,6 +217,7 @@ function main() {
|
|
|
183
217
|
url.pathname !== "/v1/calls/twilio/voice-webhook" &&
|
|
184
218
|
url.pathname !== "/v1/calls/twilio/status" &&
|
|
185
219
|
url.pathname !== "/v1/calls/twilio/connect-action" &&
|
|
220
|
+
url.pathname !== "/v1/browser-relay" &&
|
|
186
221
|
url.pathname !== "/v1/calls/relay");
|
|
187
222
|
if (isRateLimitedRoute) {
|
|
188
223
|
const clientIp = getClientIp(req, svr, config.trustProxy);
|
|
@@ -308,6 +343,13 @@ function main() {
|
|
|
308
343
|
return undefined as unknown as Response;
|
|
309
344
|
}
|
|
310
345
|
|
|
346
|
+
if (config.runtimeProxyEnabled && url.pathname === "/v1/browser-relay") {
|
|
347
|
+
const upgradeResult = handleBrowserRelayWs(req, server);
|
|
348
|
+
if (upgradeResult !== undefined) return upgradeResult;
|
|
349
|
+
// If upgrade was handled, Bun doesn't need a response
|
|
350
|
+
return undefined as unknown as Response;
|
|
351
|
+
}
|
|
352
|
+
|
|
311
353
|
if (url.pathname === "/webhooks/oauth/callback" && tracedReq.method === "GET") {
|
|
312
354
|
const res = await handleOAuthCallback(tracedReq);
|
|
313
355
|
if (res.status === 400) {
|
|
@@ -54,6 +54,9 @@ const baseConfig: GatewayConfig = {
|
|
|
54
54
|
whatsappTimeoutMs: 15000,
|
|
55
55
|
whatsappMaxRetries: 3,
|
|
56
56
|
whatsappInitialBackoffMs: 1000,
|
|
57
|
+
slackChannelBotToken: undefined,
|
|
58
|
+
slackChannelAppToken: undefined,
|
|
59
|
+
slackDeliverAuthBypass: false,
|
|
57
60
|
trustProxy: false,
|
|
58
61
|
};
|
|
59
62
|
|