@vellumai/vellum-gateway 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/ARCHITECTURE.md +67 -25
- package/Dockerfile +2 -0
- package/README.md +50 -13
- package/bun.lock +16 -2
- package/knip.json +3 -1
- package/package.json +3 -1
- package/src/__tests__/auto-approve-thresholds.test.ts +49 -22
- package/src/__tests__/channel-verification-session-proxy.test.ts +0 -1
- package/src/__tests__/config-file-watcher.test.ts +181 -0
- package/src/__tests__/config.test.ts +0 -1
- package/src/__tests__/contacts-control-plane-proxy.test.ts +0 -1
- package/src/__tests__/credential-watcher-managed-bootstrap.test.ts +10 -2
- package/src/__tests__/credential-watcher.test.ts +30 -2
- package/src/__tests__/db-connection-isolation.test.ts +157 -0
- package/src/__tests__/fake-assistant-ipc.ts +39 -0
- package/src/__tests__/feature-flags-route.test.ts +8 -8
- package/src/__tests__/guardian-init-lockfile.test.ts +30 -4
- package/src/__tests__/ipc-feature-flag-routes.test.ts +1 -1
- package/src/__tests__/live-voice-websocket.test.ts +0 -1
- package/src/__tests__/load-guards.test.ts +0 -1
- package/src/__tests__/migration-teleport-gcs-proxy.test.ts +0 -1
- package/src/__tests__/oauth-callback.test.ts +0 -1
- package/src/__tests__/pair-origin-allowlist.test.ts +155 -0
- package/src/__tests__/rate-limit-loopback.test.ts +1 -1
- package/src/__tests__/remote-feature-flag-sync.test.ts +47 -7
- package/src/__tests__/resolve-assistant.test.ts +0 -1
- package/src/__tests__/route-schema-guard.test.ts +42 -6
- package/src/__tests__/runtime-client.test.ts +0 -1
- package/src/__tests__/runtime-health-proxy.test.ts +0 -1
- package/src/__tests__/runtime-proxy-auth.test.ts +0 -1
- package/src/__tests__/runtime-proxy.test.ts +0 -1
- package/src/__tests__/slack-control-plane-proxy.test.ts +0 -1
- package/src/__tests__/slack-display-name.test.ts +66 -1
- package/src/__tests__/slack-normalize.test.ts +158 -4
- package/src/__tests__/slack-reaction-normalize.test.ts +0 -1
- package/src/__tests__/slack-socket-mode-catchup.test.ts +857 -0
- package/src/__tests__/slack-socket-mode-scopes.test.ts +52 -0
- package/src/__tests__/slack-socket-mode-thread-tracking.test.ts +654 -0
- package/src/__tests__/stt-stream-websocket.test.ts +0 -1
- package/src/__tests__/telegram-control-plane-proxy.test.ts +0 -1
- package/src/__tests__/telegram-send-attachments.test.ts +0 -1
- package/src/__tests__/telegram-webhook-handler.test.ts +0 -1
- package/src/__tests__/text-verification-helpers.test.ts +136 -0
- package/src/__tests__/twilio-media-websocket.test.ts +0 -1
- package/src/__tests__/twilio-relay-websocket.test.ts +0 -1
- package/src/__tests__/twilio-webhooks.test.ts +220 -3
- package/src/__tests__/upstream-transport.test.ts +0 -36
- package/src/__tests__/whatsapp-download.test.ts +0 -1
- package/src/__tests__/whatsapp-webhook.test.ts +0 -1
- package/src/auth/guardian-refresh.ts +4 -18
- package/src/auth/ipc-route-policy.ts +217 -0
- package/src/backup/backup-key.ts +138 -0
- package/src/backup/backup-routes.ts +159 -0
- package/src/backup/backup-worker.ts +374 -0
- package/src/backup/list-snapshots.ts +97 -0
- package/src/backup/local-writer.ts +87 -0
- package/src/backup/offsite-writer.ts +182 -0
- package/src/backup/paths.ts +123 -0
- package/src/backup/stream-crypt.ts +258 -0
- package/src/chrome-extension-origins.ts +28 -0
- package/src/cli/enable-proxy.ts +0 -1
- package/src/config-file-cache.ts +3 -19
- package/src/config-file-utils.ts +124 -0
- package/src/config-file-watcher.ts +57 -25
- package/src/config.ts +4 -7
- package/src/db/connection.ts +65 -3
- package/src/db/contact-store.ts +30 -1
- package/src/db/data-migrations/index.ts +2 -0
- package/src/db/data-migrations/m0003-recover-backup-key.ts +71 -0
- package/src/db/schema.ts +92 -0
- package/src/db/slack-store.ts +144 -11
- package/src/feature-flag-registry.json +40 -152
- package/src/handlers/handle-inbound.ts +123 -0
- package/src/http/middleware/auth.ts +44 -1
- package/src/http/middleware/cors.ts +84 -0
- package/src/http/middleware/rate-limit.ts +6 -8
- package/src/http/routes/auto-approve-thresholds.ts +17 -1
- package/src/http/routes/brain-graph-proxy.ts +1 -1
- package/src/http/routes/channel-readiness-proxy.ts +2 -2
- package/src/http/routes/channel-verification-session-proxy.ts +19 -37
- package/src/http/routes/contact-prompt.ts +149 -0
- package/src/http/routes/contacts-control-plane-proxy.ts +2 -2
- package/src/http/routes/email-webhook.test.ts +0 -1
- package/src/http/routes/ipc-runtime-proxy.test.ts +197 -1
- package/src/http/routes/ipc-runtime-proxy.ts +95 -0
- package/src/http/routes/log-export.test.ts +0 -1
- package/src/http/routes/log-tail.test.ts +336 -0
- package/src/http/routes/log-tail.ts +87 -0
- package/src/http/routes/migration-proxy.ts +1 -2
- package/src/http/routes/oauth-apps-proxy.ts +2 -2
- package/src/http/routes/oauth-providers-proxy.ts +2 -2
- package/src/http/routes/pair.ts +322 -0
- package/src/http/routes/privacy-config.ts +65 -79
- package/src/http/routes/runtime-health-proxy.ts +2 -2
- package/src/http/routes/runtime-proxy.ts +3 -1
- package/src/http/routes/slack-control-plane-proxy.ts +3 -20
- package/src/http/routes/stt-stream-websocket.ts +2 -3
- package/src/http/routes/telegram-control-plane-proxy.ts +2 -2
- package/src/http/routes/telegram-webhook.test.ts +0 -1
- package/src/http/routes/telegram-webhook.ts +6 -0
- package/src/http/routes/trust-rules.suggest.test.ts +25 -0
- package/src/http/routes/trust-rules.ts +7 -0
- package/src/http/routes/twilio-control-plane-proxy.ts +2 -2
- package/src/http/routes/twilio-media-websocket.ts +5 -5
- package/src/http/routes/twilio-voice-verify-callback.ts +310 -0
- package/src/http/routes/twilio-voice-webhook.test.ts +65 -1
- package/src/http/routes/twilio-voice-webhook.ts +45 -1
- package/src/http/routes/whatsapp-webhook.test.ts +0 -1
- package/src/index.ts +357 -278
- package/src/ipc/assistant-client.ts +8 -4
- package/src/ipc/contact-handlers.ts +88 -3
- package/src/ipc/threshold-handlers.ts +2 -0
- package/src/post-assistant-ready.ts +5 -3
- package/src/risk/bash-risk-classifier.test.ts +35 -27
- package/src/risk/bash-risk-classifier.ts +44 -14
- package/src/risk/command-registry/commands/assistant.ts +8 -19
- package/src/risk/command-registry.test.ts +0 -15
- package/src/risk/risk-classifier-parity.test.ts +1 -3
- package/src/runtime/client.ts +58 -3
- package/src/schema.ts +277 -104
- package/src/slack/normalize.test.ts +98 -0
- package/src/slack/normalize.ts +107 -32
- package/src/slack/slack-web.ts +213 -0
- package/src/slack/socket-mode.ts +701 -39
- package/src/telegram/send.test.ts +0 -1
- package/src/twilio/validate-webhook.ts +53 -14
- package/src/twilio/webhook-sync-trigger.ts +58 -0
- package/src/twilio/webhook-sync.test.ts +286 -0
- package/src/twilio/webhook-sync.ts +84 -0
- package/src/util/is-loopback-address.ts +27 -0
- package/src/velay/bridge-utils.ts +228 -0
- package/src/velay/client.test.ts +939 -0
- package/src/velay/client.ts +555 -0
- package/src/velay/http-bridge.test.ts +217 -0
- package/src/velay/http-bridge.ts +83 -0
- package/src/velay/protocol.ts +178 -0
- package/src/velay/test-fake-websocket.ts +69 -0
- package/src/velay/websocket-bridge.test.ts +367 -0
- package/src/velay/websocket-bridge.ts +324 -0
- package/src/verification/binding-helpers.ts +107 -0
- package/src/verification/code-parsing.ts +44 -0
- package/src/verification/contact-helpers.ts +342 -0
- package/src/verification/identity-match.ts +68 -0
- package/src/verification/identity.ts +61 -0
- package/src/verification/rate-limit-helpers.ts +205 -0
- package/src/verification/reply-delivery.ts +109 -0
- package/src/verification/session-helpers.ts +164 -0
- package/src/verification/text-verification.ts +372 -0
- package/src/version.ts +35 -0
- package/src/voice/verification.ts +456 -0
- package/src/webhook-pipeline.ts +4 -0
- package/src/__tests__/browser-relay-websocket.test.ts +0 -698
- package/src/__tests__/telegram-only-default.test.ts +0 -133
- package/src/auth/capability-tokens.ts +0 -248
- package/src/http/routes/browser-extension-pair.ts +0 -455
- package/src/http/routes/browser-relay-websocket.ts +0 -381
- package/src/http/routes/config-file-utils.ts +0 -73
- package/src/ipc/capability-token-handlers.ts +0 -30
- package/src/pairing/approved-devices-store.ts +0 -110
- package/src/pairing/pairing-routes.ts +0 -379
- package/src/pairing/pairing-store.ts +0 -218
package/src/index.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
process.title = "vellum-gateway";
|
|
2
2
|
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
TWILIO_CONNECT_ACTION_WEBHOOK_PATH,
|
|
7
|
+
TWILIO_MEDIA_STREAM_WEBHOOK_PATH,
|
|
8
|
+
TWILIO_RELAY_WEBHOOK_PATH,
|
|
9
|
+
TWILIO_STATUS_WEBHOOK_PATH,
|
|
10
|
+
TWILIO_VOICE_WEBHOOK_PATH,
|
|
11
|
+
} from "@vellumai/service-contracts/twilio-ingress";
|
|
12
|
+
|
|
4
13
|
import { AuthRateLimiter } from "./auth-rate-limiter.js";
|
|
5
14
|
import {
|
|
6
15
|
loadOrCreateSigningKey,
|
|
@@ -20,16 +29,13 @@ import {
|
|
|
20
29
|
type CredentialChangeEvent,
|
|
21
30
|
} from "./credential-watcher.js";
|
|
22
31
|
import { createRuntimeProxyHandler } from "./http/routes/runtime-proxy.js";
|
|
23
|
-
|
|
24
|
-
createBrowserRelayWebsocketHandler,
|
|
25
|
-
getBrowserRelayWebsocketHandlers,
|
|
26
|
-
type BrowserRelaySocketData,
|
|
27
|
-
} from "./http/routes/browser-relay-websocket.js";
|
|
32
|
+
|
|
28
33
|
import { createTelegramWebhookHandler } from "./http/routes/telegram-webhook.js";
|
|
29
34
|
import { createAudioProxyHandler } from "./http/routes/audio-proxy.js";
|
|
30
35
|
import { createTwilioVoiceWebhookHandler } from "./http/routes/twilio-voice-webhook.js";
|
|
31
36
|
import { createTwilioStatusWebhookHandler } from "./http/routes/twilio-status-webhook.js";
|
|
32
37
|
import { createTwilioConnectActionWebhookHandler } from "./http/routes/twilio-connect-action-webhook.js";
|
|
38
|
+
import { createTwilioVoiceVerifyCallbackHandler } from "./http/routes/twilio-voice-verify-callback.js";
|
|
33
39
|
import {
|
|
34
40
|
createTwilioRelayWebsocketHandler,
|
|
35
41
|
getRelayWebsocketHandlers,
|
|
@@ -57,8 +63,6 @@ import { createMailgunWebhookHandler } from "./http/routes/mailgun-webhook.js";
|
|
|
57
63
|
import { createResendWebhookHandler } from "./http/routes/resend-webhook.js";
|
|
58
64
|
|
|
59
65
|
import { createOAuthCallbackHandler } from "./http/routes/oauth-callback.js";
|
|
60
|
-
import { createPairingHandler } from "./pairing/pairing-routes.js";
|
|
61
|
-
import { PairingStore } from "./pairing/pairing-store.js";
|
|
62
66
|
import {
|
|
63
67
|
createFeatureFlagsGetHandler,
|
|
64
68
|
createFeatureFlagsPatchHandler,
|
|
@@ -74,12 +78,13 @@ import {
|
|
|
74
78
|
createConversationThresholdPutHandler,
|
|
75
79
|
createConversationThresholdDeleteHandler,
|
|
76
80
|
} from "./http/routes/auto-approve-thresholds.js";
|
|
77
|
-
import { handleBrowserExtensionPair } from "./http/routes/browser-extension-pair.js";
|
|
78
81
|
import { createChannelVerificationSessionProxyHandler } from "./http/routes/channel-verification-session-proxy.js";
|
|
79
82
|
import { createTelegramControlPlaneProxyHandler } from "./http/routes/telegram-control-plane-proxy.js";
|
|
80
83
|
import { createTwilioControlPlaneProxyHandler } from "./http/routes/twilio-control-plane-proxy.js";
|
|
81
84
|
import { createVercelControlPlaneProxyHandler } from "./http/routes/vercel-control-plane-proxy.js";
|
|
82
85
|
import { createContactsControlPlaneProxyHandler } from "./http/routes/contacts-control-plane-proxy.js";
|
|
86
|
+
import { handleContactPromptSubmit } from "./http/routes/contact-prompt.js";
|
|
87
|
+
import { handlePair } from "./http/routes/pair.js";
|
|
83
88
|
import { createSlackControlPlaneProxyHandler } from "./http/routes/slack-control-plane-proxy.js";
|
|
84
89
|
import { createOAuthAppsProxyHandler } from "./http/routes/oauth-apps-proxy.js";
|
|
85
90
|
import { createOAuthProvidersProxyHandler } from "./http/routes/oauth-providers-proxy.js";
|
|
@@ -96,9 +101,15 @@ import {
|
|
|
96
101
|
createMigrationJobStatusProxyHandler,
|
|
97
102
|
} from "./http/routes/migration-proxy.js";
|
|
98
103
|
import { createMigrationRollbackProxyHandler } from "./http/routes/migration-rollback-proxy.js";
|
|
104
|
+
import {
|
|
105
|
+
createListBackupsHandler,
|
|
106
|
+
createBackupSnapshotHandler,
|
|
107
|
+
} from "./backup/backup-routes.js";
|
|
108
|
+
import { startBackupWorker } from "./backup/backup-worker.js";
|
|
99
109
|
import { createWorkspaceCommitProxyHandler } from "./http/routes/workspace-commit-proxy.js";
|
|
100
110
|
import { createBrainGraphProxyHandler } from "./http/routes/brain-graph-proxy.js";
|
|
101
111
|
import { createLogExportHandler } from "./http/routes/log-export.js";
|
|
112
|
+
import { createLogTailHandler } from "./http/routes/log-tail.js";
|
|
102
113
|
import {
|
|
103
114
|
createTrustRulesListHandler,
|
|
104
115
|
createTrustRulesCreateHandler,
|
|
@@ -122,8 +133,13 @@ import {
|
|
|
122
133
|
} from "./slack/socket-mode.js";
|
|
123
134
|
import { downloadSlackFile } from "./slack/download.js";
|
|
124
135
|
import { handleInbound } from "./handlers/handle-inbound.js";
|
|
136
|
+
import { upsertContactChannel } from "./verification/contact-helpers.js";
|
|
125
137
|
import { checkAuthRateLimit } from "./http/middleware/rate-limit.js";
|
|
138
|
+
import { logAuthBypassState } from "./http/middleware/auth.js";
|
|
126
139
|
import {
|
|
140
|
+
resolveExtensionOrigin,
|
|
141
|
+
handleExtensionPreflight,
|
|
142
|
+
withExtensionCorsHeaders,
|
|
127
143
|
resolveWebviewOrigin,
|
|
128
144
|
handlePreflight,
|
|
129
145
|
withCorsHeaders,
|
|
@@ -139,14 +155,16 @@ import { fetchImpl } from "./fetch.js";
|
|
|
139
155
|
import { isNewCommand, handleNewCommand } from "./webhook-pipeline.js";
|
|
140
156
|
import { reconcileTelegramWebhook } from "./telegram/webhook-manager.js";
|
|
141
157
|
import { registerEmailCallbackRoute } from "./email/register-callback.js";
|
|
158
|
+
import { syncConfiguredTwilioPhoneNumberWebhooks } from "./twilio/webhook-sync.js";
|
|
159
|
+
import {
|
|
160
|
+
isOnlyVelayPublicBaseUrlChange,
|
|
161
|
+
shouldSyncTwilioPhoneWebhooksAfterConfigChange,
|
|
162
|
+
} from "./twilio/webhook-sync-trigger.js";
|
|
142
163
|
import { GatewayIpcServer } from "./ipc/server.js";
|
|
143
164
|
import { contactRoutes } from "./ipc/contact-handlers.js";
|
|
144
|
-
import {
|
|
145
|
-
featureFlagRoutes,
|
|
146
|
-
getMergedFeatureFlags,
|
|
147
|
-
} from "./ipc/feature-flag-handlers.js";
|
|
165
|
+
import { featureFlagRoutes } from "./ipc/feature-flag-handlers.js";
|
|
148
166
|
import { thresholdRoutes } from "./ipc/threshold-handlers.js";
|
|
149
|
-
|
|
167
|
+
|
|
150
168
|
import { riskClassificationRoutes } from "./ipc/risk-classification-handlers.js";
|
|
151
169
|
import { refreshRouteSchema } from "./ipc/route-schema-cache.js";
|
|
152
170
|
import { AvatarChannelSyncer } from "./avatar-sync/avatar-channel-syncer.js";
|
|
@@ -154,6 +172,8 @@ import { AvatarSyncWatcher } from "./avatar-sync/avatar-sync-watcher.js";
|
|
|
154
172
|
import { SlackAvatarSyncer } from "./avatar-sync/slack-avatar-syncer.js";
|
|
155
173
|
import { initGatewayDb } from "./db/connection.js";
|
|
156
174
|
import { runPostAssistantReady } from "./post-assistant-ready.js";
|
|
175
|
+
import { createVelayTunnelClient } from "./velay/client.js";
|
|
176
|
+
import { VERSION_HEADER_NAME, VERSION_HEADER_VALUE } from "./version.js";
|
|
157
177
|
|
|
158
178
|
const log = getLogger("main");
|
|
159
179
|
|
|
@@ -197,16 +217,6 @@ function detectCredentialChanges(
|
|
|
197
217
|
// Shared rate limiter for auth failures and unauthenticated endpoints
|
|
198
218
|
const authRateLimiter = new AuthRateLimiter();
|
|
199
219
|
|
|
200
|
-
function isBrowserRelaySocketData(
|
|
201
|
-
data: unknown,
|
|
202
|
-
): data is BrowserRelaySocketData {
|
|
203
|
-
return (
|
|
204
|
-
!!data &&
|
|
205
|
-
typeof data === "object" &&
|
|
206
|
-
(data as { wsType?: unknown }).wsType === "browser-relay"
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
220
|
function isMediaStreamSocketData(data: unknown): data is MediaStreamSocketData {
|
|
211
221
|
return (
|
|
212
222
|
!!data &&
|
|
@@ -262,12 +272,23 @@ async function main() {
|
|
|
262
272
|
await initGatewayDb();
|
|
263
273
|
initTrustRuleCache();
|
|
264
274
|
|
|
275
|
+
// Wait for the assistant runtime to be healthy before serving traffic.
|
|
276
|
+
// Data migrations (e.g. m0002 actor-token-tables-to-gateway) must
|
|
277
|
+
// complete before the HTTP server starts accepting auth requests —
|
|
278
|
+
// otherwise newly minted tokens can be overwritten by stale rows
|
|
279
|
+
// migrated from the assistant DB.
|
|
280
|
+
await runPostAssistantReady();
|
|
281
|
+
|
|
265
282
|
// ── TTL caches ──
|
|
266
283
|
// Instantiate caches for credential and config file reads.
|
|
267
284
|
// Handlers read dynamic credentials and config.json values from these
|
|
268
285
|
// caches at call time, with automatic TTL refresh.
|
|
269
286
|
const credentialCache = new CredentialCache();
|
|
270
287
|
const configFileCache = new ConfigFileCache();
|
|
288
|
+
const velayTunnelClient = createVelayTunnelClient(config, {
|
|
289
|
+
credentials: credentialCache,
|
|
290
|
+
configFile: configFileCache,
|
|
291
|
+
});
|
|
271
292
|
|
|
272
293
|
// ── Avatar sync ──
|
|
273
294
|
const avatarChannelSyncer = new AvatarChannelSyncer();
|
|
@@ -305,18 +326,18 @@ async function main() {
|
|
|
305
326
|
);
|
|
306
327
|
const handleTwilioConnectActionWebhook =
|
|
307
328
|
createTwilioConnectActionWebhookHandler(config, twilioValidationCaches);
|
|
329
|
+
const handleTwilioVoiceVerifyCallback =
|
|
330
|
+
createTwilioVoiceVerifyCallbackHandler(config, twilioValidationCaches);
|
|
308
331
|
const handleTwilioRelayWs = createTwilioRelayWebsocketHandler(config, {
|
|
309
332
|
configFile: configFileCache,
|
|
310
333
|
});
|
|
311
334
|
const handleTwilioMediaWs = createTwilioMediaWebsocketHandler(config, {
|
|
312
335
|
configFile: configFileCache,
|
|
313
336
|
});
|
|
314
|
-
const handleBrowserRelayWs = createBrowserRelayWebsocketHandler(config);
|
|
315
337
|
const handleSttStreamWs = createSttStreamWebsocketHandler(config);
|
|
316
338
|
const handleLiveVoiceWs = createLiveVoiceWebsocketHandler(config);
|
|
317
339
|
const twilioRelayWebsocketHandlers = getRelayWebsocketHandlers();
|
|
318
340
|
const twilioMediaStreamWebsocketHandlers = getMediaStreamWebsocketHandlers();
|
|
319
|
-
const browserRelayWebsocketHandlers = getBrowserRelayWebsocketHandlers();
|
|
320
341
|
const sttStreamWebsocketHandlers = getSttStreamWebsocketHandlers();
|
|
321
342
|
const liveVoiceWebsocketHandlers = getLiveVoiceWebsocketHandlers();
|
|
322
343
|
const { handler: handleWhatsAppWebhook, dedupCache: whatsappDedupCache } =
|
|
@@ -345,9 +366,6 @@ async function main() {
|
|
|
345
366
|
credentialCache,
|
|
346
367
|
);
|
|
347
368
|
const handleOAuthCallback = createOAuthCallbackHandler(config);
|
|
348
|
-
const pairingStore = new PairingStore();
|
|
349
|
-
pairingStore.start();
|
|
350
|
-
const pairingHandler = createPairingHandler({ pairingStore });
|
|
351
369
|
const channelVerificationSessionProxy =
|
|
352
370
|
createChannelVerificationSessionProxyHandler(config);
|
|
353
371
|
const telegramControlPlaneProxy =
|
|
@@ -376,6 +394,7 @@ async function main() {
|
|
|
376
394
|
const workspaceCommitProxy = createWorkspaceCommitProxyHandler(config);
|
|
377
395
|
const brainGraphProxy = createBrainGraphProxyHandler(config);
|
|
378
396
|
const handleLogExport = createLogExportHandler(config);
|
|
397
|
+
const handleLogTail = createLogTailHandler(config);
|
|
379
398
|
const handleFeatureFlagsGet = createFeatureFlagsGetHandler();
|
|
380
399
|
const handleFeatureFlagsPatch = createFeatureFlagsPatchHandler();
|
|
381
400
|
const handlePrivacyConfigGet = createPrivacyConfigGetHandler();
|
|
@@ -397,9 +416,11 @@ async function main() {
|
|
|
397
416
|
|
|
398
417
|
const audioProxy = createAudioProxyHandler(config);
|
|
399
418
|
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
419
|
+
const backupDeps = { assistantRuntimeBaseUrl: config.assistantRuntimeBaseUrl };
|
|
420
|
+
const handleListBackups = createListBackupsHandler(backupDeps);
|
|
421
|
+
const handleCreateBackup = createBackupSnapshotHandler(backupDeps);
|
|
422
|
+
|
|
423
|
+
const handleRuntimeProxy = createRuntimeProxyHandler(config);
|
|
403
424
|
|
|
404
425
|
// Helper to reject when an integration isn't configured
|
|
405
426
|
const requireConfigured = (
|
|
@@ -436,17 +457,21 @@ async function main() {
|
|
|
436
457
|
handler: (req) => handleTelegramWebhook(req),
|
|
437
458
|
},
|
|
438
459
|
{
|
|
439
|
-
path:
|
|
460
|
+
path: TWILIO_VOICE_WEBHOOK_PATH,
|
|
440
461
|
handler: (req) => handleTwilioVoiceWebhook(req),
|
|
441
462
|
},
|
|
442
463
|
{
|
|
443
|
-
path:
|
|
464
|
+
path: TWILIO_STATUS_WEBHOOK_PATH,
|
|
444
465
|
handler: (req) => handleTwilioStatusWebhook(req),
|
|
445
466
|
},
|
|
446
467
|
{
|
|
447
|
-
path:
|
|
468
|
+
path: TWILIO_CONNECT_ACTION_WEBHOOK_PATH,
|
|
448
469
|
handler: (req) => handleTwilioConnectActionWebhook(req),
|
|
449
470
|
},
|
|
471
|
+
{
|
|
472
|
+
path: "/webhooks/twilio/voice-verify",
|
|
473
|
+
handler: (req) => handleTwilioVoiceVerifyCallback(req),
|
|
474
|
+
},
|
|
450
475
|
{
|
|
451
476
|
path: "/webhooks/whatsapp",
|
|
452
477
|
precondition: requireWhatsApp,
|
|
@@ -469,7 +494,8 @@ async function main() {
|
|
|
469
494
|
{
|
|
470
495
|
path: "/inbound/register",
|
|
471
496
|
method: "POST",
|
|
472
|
-
auth: "edge",
|
|
497
|
+
auth: "edge-scoped",
|
|
498
|
+
scope: "internal.write",
|
|
473
499
|
handler: (req) => handleInboundRegister(req),
|
|
474
500
|
},
|
|
475
501
|
|
|
@@ -487,28 +513,6 @@ async function main() {
|
|
|
487
513
|
handler: (req) => handleOAuthCallback(req),
|
|
488
514
|
},
|
|
489
515
|
|
|
490
|
-
// ── Pairing (mixed auth) ──
|
|
491
|
-
{
|
|
492
|
-
path: "/pairing/register",
|
|
493
|
-
method: "POST",
|
|
494
|
-
auth: "edge",
|
|
495
|
-
handler: (req) => pairingHandler.handlePairingRegister(req),
|
|
496
|
-
},
|
|
497
|
-
{
|
|
498
|
-
path: "/pairing/request",
|
|
499
|
-
method: "POST",
|
|
500
|
-
auth: "track-failures",
|
|
501
|
-
trackFailureStatuses: [401, 403],
|
|
502
|
-
handler: (req) => pairingHandler.handlePairingRequest(req),
|
|
503
|
-
},
|
|
504
|
-
{
|
|
505
|
-
path: "/pairing/status",
|
|
506
|
-
method: "GET",
|
|
507
|
-
auth: "track-failures",
|
|
508
|
-
trackFailureStatuses: [401, 403],
|
|
509
|
-
handler: (req) => pairingHandler.handlePairingStatus(req),
|
|
510
|
-
},
|
|
511
|
-
|
|
512
516
|
// ── Runtime health ──
|
|
513
517
|
{
|
|
514
518
|
path: "/v1/health",
|
|
@@ -599,6 +603,12 @@ async function main() {
|
|
|
599
603
|
},
|
|
600
604
|
|
|
601
605
|
// ── Contacts control plane ──
|
|
606
|
+
{
|
|
607
|
+
path: "/v1/contacts/prompt/submit",
|
|
608
|
+
method: "POST",
|
|
609
|
+
auth: "edge",
|
|
610
|
+
handler: (req) => handleContactPromptSubmit(req),
|
|
611
|
+
},
|
|
602
612
|
{
|
|
603
613
|
path: "/v1/contacts",
|
|
604
614
|
method: "GET",
|
|
@@ -674,13 +684,12 @@ async function main() {
|
|
|
674
684
|
contactsControlPlaneProxy.handleGetContact(req, params[0]),
|
|
675
685
|
},
|
|
676
686
|
|
|
677
|
-
// ──
|
|
687
|
+
// ── Generic loopback pairing (localhost-only, auth: none) ──
|
|
678
688
|
{
|
|
679
|
-
path: "/v1/
|
|
689
|
+
path: "/v1/pair",
|
|
680
690
|
method: "POST",
|
|
681
691
|
auth: "none",
|
|
682
|
-
handler: (req, _params, getClientIp) =>
|
|
683
|
-
handleBrowserExtensionPair(req, getClientIp()),
|
|
692
|
+
handler: (req, _params, getClientIp) => handlePair(req, getClientIp()),
|
|
684
693
|
},
|
|
685
694
|
|
|
686
695
|
// ── Channel verification sessions ──
|
|
@@ -827,13 +836,6 @@ async function main() {
|
|
|
827
836
|
auth: "edge",
|
|
828
837
|
handler: (req) => slackControlPlaneProxy.handleShareToSlack(req),
|
|
829
838
|
},
|
|
830
|
-
{
|
|
831
|
-
path: "/v1/integrations/slack/channel/oauth-install",
|
|
832
|
-
method: "POST",
|
|
833
|
-
auth: "edge-scoped",
|
|
834
|
-
scope: "settings.write",
|
|
835
|
-
handler: (req) => slackControlPlaneProxy.handleSlackOAuthInstall(req),
|
|
836
|
-
},
|
|
837
839
|
|
|
838
840
|
// ── OAuth providers ──
|
|
839
841
|
{
|
|
@@ -948,10 +950,10 @@ async function main() {
|
|
|
948
950
|
},
|
|
949
951
|
|
|
950
952
|
// ── Teleport-GCS migration (unified daemon-async flow) ──
|
|
951
|
-
//
|
|
952
|
-
//
|
|
953
|
-
//
|
|
954
|
-
//
|
|
953
|
+
// Registered as explicit routes (not via the runtime-proxy catch-all)
|
|
954
|
+
// for dedicated auth and timeout handling. The daemon returns 202
|
|
955
|
+
// { job_id } on POST and cheap JSON on GET, so the gateway just
|
|
956
|
+
// transparently forwards without wrapping.
|
|
955
957
|
{
|
|
956
958
|
path: "/v1/migrations/export-to-gcs",
|
|
957
959
|
method: "POST",
|
|
@@ -992,6 +994,22 @@ async function main() {
|
|
|
992
994
|
handler: (req) => migrationRollbackProxy(req),
|
|
993
995
|
},
|
|
994
996
|
|
|
997
|
+
// ── Backups ──
|
|
998
|
+
{
|
|
999
|
+
path: "/v1/backups",
|
|
1000
|
+
method: "GET",
|
|
1001
|
+
auth: "edge-scoped",
|
|
1002
|
+
scope: "settings.read",
|
|
1003
|
+
handler: (req) => handleListBackups(req),
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
path: "/v1/backups/create",
|
|
1007
|
+
method: "POST",
|
|
1008
|
+
auth: "edge-scoped",
|
|
1009
|
+
scope: "settings.write",
|
|
1010
|
+
handler: (req) => handleCreateBackup(req),
|
|
1011
|
+
},
|
|
1012
|
+
|
|
995
1013
|
// ── Channel readiness ──
|
|
996
1014
|
{
|
|
997
1015
|
path: "/v1/channels/readiness",
|
|
@@ -1205,6 +1223,12 @@ async function main() {
|
|
|
1205
1223
|
handler: (req, params, getClientIp) =>
|
|
1206
1224
|
handleLogExport(req, params, getClientIp),
|
|
1207
1225
|
},
|
|
1226
|
+
{
|
|
1227
|
+
path: "/v1/logs/tail",
|
|
1228
|
+
method: "GET",
|
|
1229
|
+
auth: "edge",
|
|
1230
|
+
handler: (req) => handleLogTail(req),
|
|
1231
|
+
},
|
|
1208
1232
|
|
|
1209
1233
|
// ── Trust rules v3 ──
|
|
1210
1234
|
{
|
|
@@ -1248,21 +1272,24 @@ async function main() {
|
|
|
1248
1272
|
},
|
|
1249
1273
|
];
|
|
1250
1274
|
|
|
1251
|
-
//
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
handleRuntimeProxy(req, getClientIp()),
|
|
1259
|
-
});
|
|
1260
|
-
}
|
|
1275
|
+
// Runtime proxy catch-all — must be last so specific routes are checked first.
|
|
1276
|
+
routes.push({
|
|
1277
|
+
path: /^\//, // match everything
|
|
1278
|
+
auth: "track-failures",
|
|
1279
|
+
handler: (req, _params, getClientIp) =>
|
|
1280
|
+
handleRuntimeProxy(req, getClientIp()),
|
|
1281
|
+
});
|
|
1261
1282
|
|
|
1262
1283
|
const router = createRouter(routes, {
|
|
1263
1284
|
authRateLimiter,
|
|
1264
1285
|
});
|
|
1265
1286
|
|
|
1287
|
+
/** Stamp the assistant version header on a response. */
|
|
1288
|
+
function stampVersion<T extends Response>(res: T): T {
|
|
1289
|
+
res.headers.set(VERSION_HEADER_NAME, VERSION_HEADER_VALUE);
|
|
1290
|
+
return res;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1266
1293
|
const server = Bun.serve({
|
|
1267
1294
|
port: config.port,
|
|
1268
1295
|
idleTimeout: 0,
|
|
@@ -1271,10 +1298,6 @@ async function main() {
|
|
|
1271
1298
|
maxRequestBodySize: 512 * 1024 * 1024,
|
|
1272
1299
|
websocket: {
|
|
1273
1300
|
open(ws) {
|
|
1274
|
-
if (isBrowserRelaySocketData(ws.data)) {
|
|
1275
|
-
browserRelayWebsocketHandlers.open(ws as never);
|
|
1276
|
-
return;
|
|
1277
|
-
}
|
|
1278
1301
|
if (isMediaStreamSocketData(ws.data)) {
|
|
1279
1302
|
twilioMediaStreamWebsocketHandlers.open(ws as never);
|
|
1280
1303
|
return;
|
|
@@ -1290,10 +1313,6 @@ async function main() {
|
|
|
1290
1313
|
twilioRelayWebsocketHandlers.open(ws as never);
|
|
1291
1314
|
},
|
|
1292
1315
|
message(ws, message) {
|
|
1293
|
-
if (isBrowserRelaySocketData(ws.data)) {
|
|
1294
|
-
browserRelayWebsocketHandlers.message(ws as never, message);
|
|
1295
|
-
return;
|
|
1296
|
-
}
|
|
1297
1316
|
if (isMediaStreamSocketData(ws.data)) {
|
|
1298
1317
|
twilioMediaStreamWebsocketHandlers.message(ws as never, message);
|
|
1299
1318
|
return;
|
|
@@ -1309,10 +1328,6 @@ async function main() {
|
|
|
1309
1328
|
twilioRelayWebsocketHandlers.message(ws as never, message);
|
|
1310
1329
|
},
|
|
1311
1330
|
close(ws, code, reason) {
|
|
1312
|
-
if (isBrowserRelaySocketData(ws.data)) {
|
|
1313
|
-
browserRelayWebsocketHandlers.close(ws as never, code, reason);
|
|
1314
|
-
return;
|
|
1315
|
-
}
|
|
1316
1331
|
if (isMediaStreamSocketData(ws.data)) {
|
|
1317
1332
|
twilioMediaStreamWebsocketHandlers.close(ws as never, code, reason);
|
|
1318
1333
|
return;
|
|
@@ -1330,210 +1345,228 @@ async function main() {
|
|
|
1330
1345
|
},
|
|
1331
1346
|
error(err) {
|
|
1332
1347
|
if (err instanceof CircuitBreakerOpenError) {
|
|
1333
|
-
return
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1348
|
+
return stampVersion(
|
|
1349
|
+
Response.json(
|
|
1350
|
+
{
|
|
1351
|
+
error: "Service temporarily unavailable — runtime is unreachable",
|
|
1352
|
+
},
|
|
1353
|
+
{
|
|
1354
|
+
status: 503,
|
|
1355
|
+
headers: { "Retry-After": String(err.retryAfterSecs) },
|
|
1356
|
+
},
|
|
1357
|
+
),
|
|
1339
1358
|
);
|
|
1340
1359
|
}
|
|
1341
1360
|
log.error({ err }, "Unhandled gateway error");
|
|
1342
|
-
return
|
|
1361
|
+
return stampVersion(
|
|
1362
|
+
Response.json({ error: "Internal server error" }, { status: 500 }),
|
|
1363
|
+
);
|
|
1343
1364
|
},
|
|
1344
1365
|
async fetch(req, svr) {
|
|
1345
1366
|
svr.timeout(req, 1800);
|
|
1346
|
-
const
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
// Reflect the origin back on matched requests so window.vellum.fetch
|
|
1352
|
-
// calls succeed.
|
|
1353
|
-
const webviewOrigin = resolveWebviewOrigin(req);
|
|
1354
|
-
if (webviewOrigin && req.method === "OPTIONS") {
|
|
1355
|
-
return handlePreflight(webviewOrigin);
|
|
1356
|
-
}
|
|
1367
|
+
const inner = await routeRequest(req, svr);
|
|
1368
|
+
if (inner) stampVersion(inner);
|
|
1369
|
+
return inner;
|
|
1370
|
+
},
|
|
1371
|
+
});
|
|
1357
1372
|
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1373
|
+
/** Core request routing — extracted so `fetch` can stamp headers on every response. */
|
|
1374
|
+
async function routeRequest(
|
|
1375
|
+
req: Request,
|
|
1376
|
+
svr: ReturnType<typeof Bun.serve>,
|
|
1377
|
+
): Promise<Response | undefined> {
|
|
1378
|
+
const url = new URL(req.url);
|
|
1379
|
+
|
|
1380
|
+
// ── CORS: webview preflight & origin tracking ──
|
|
1381
|
+
// The macOS WKWebView loads pages from https://{appId}.vellum.local/
|
|
1382
|
+
// which is cross-origin to the gateway at http://127.0.0.1:{port}.
|
|
1383
|
+
// Reflect the origin back on matched requests so window.vellum.fetch
|
|
1384
|
+
// calls succeed.
|
|
1385
|
+
const extensionOrigin = resolveExtensionOrigin(req);
|
|
1386
|
+
if (extensionOrigin && req.method === "OPTIONS") {
|
|
1387
|
+
return handleExtensionPreflight(extensionOrigin);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
const webviewOrigin = resolveWebviewOrigin(req);
|
|
1391
|
+
if (webviewOrigin && req.method === "OPTIONS") {
|
|
1392
|
+
return handlePreflight(webviewOrigin);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// ── Pre-router: health/readiness probes ──
|
|
1396
|
+
// These bypass rate limiting and tracing for minimal overhead.
|
|
1397
|
+
if (url.pathname === "/healthz") {
|
|
1398
|
+
const includeMigrations =
|
|
1399
|
+
url.searchParams.get("include") === "migrations";
|
|
1400
|
+
if (!includeMigrations) {
|
|
1401
|
+
return Response.json({ status: "ok" });
|
|
1402
|
+
}
|
|
1403
|
+
// Fetch the daemon's /v1/health to surface migration state
|
|
1404
|
+
// (dbVersion, lastWorkspaceMigrationId) so the CLI can capture
|
|
1405
|
+
// pre-upgrade migration state through the gateway.
|
|
1406
|
+
try {
|
|
1407
|
+
const upstream = await fetch(
|
|
1408
|
+
`${config.assistantRuntimeBaseUrl}/v1/health`,
|
|
1409
|
+
{
|
|
1410
|
+
signal: AbortSignal.timeout(3000),
|
|
1411
|
+
headers: { authorization: `Bearer ${mintServiceToken()}` },
|
|
1412
|
+
},
|
|
1413
|
+
);
|
|
1414
|
+
if (upstream.ok) {
|
|
1415
|
+
const body = (await upstream.json()) as {
|
|
1416
|
+
migrations?: {
|
|
1417
|
+
dbVersion?: number;
|
|
1418
|
+
lastWorkspaceMigrationId?: string;
|
|
1383
1419
|
};
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
})
|
|
1388
|
-
}
|
|
1389
|
-
} catch {
|
|
1390
|
-
// Daemon unreachable — graceful degradation, still return ok
|
|
1420
|
+
};
|
|
1421
|
+
return Response.json({
|
|
1422
|
+
status: "ok",
|
|
1423
|
+
...(body.migrations ? { migrations: body.migrations } : {}),
|
|
1424
|
+
});
|
|
1391
1425
|
}
|
|
1392
|
-
|
|
1426
|
+
} catch {
|
|
1427
|
+
// Daemon unreachable — graceful degradation, still return ok
|
|
1393
1428
|
}
|
|
1429
|
+
return Response.json({ status: "ok" });
|
|
1430
|
+
}
|
|
1394
1431
|
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1432
|
+
if (url.pathname === "/schema") {
|
|
1433
|
+
return Response.json(buildSchema());
|
|
1434
|
+
}
|
|
1398
1435
|
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
return Response.json(
|
|
1412
|
-
{ status: "upstream_unhealthy", upstream: upstream.status },
|
|
1413
|
-
{ status: 503 },
|
|
1414
|
-
);
|
|
1415
|
-
}
|
|
1416
|
-
} catch {
|
|
1436
|
+
if (url.pathname === "/readyz") {
|
|
1437
|
+
if (draining) {
|
|
1438
|
+
return Response.json({ status: "draining" }, { status: 503 });
|
|
1439
|
+
}
|
|
1440
|
+
// Check that the upstream assistant is also reachable so callers
|
|
1441
|
+
// know the full stack is ready, not just the gateway process.
|
|
1442
|
+
try {
|
|
1443
|
+
const upstream = await fetch(
|
|
1444
|
+
`${config.assistantRuntimeBaseUrl}/readyz`,
|
|
1445
|
+
{ signal: AbortSignal.timeout(3000) },
|
|
1446
|
+
);
|
|
1447
|
+
if (!upstream.ok) {
|
|
1417
1448
|
return Response.json(
|
|
1418
|
-
{ status: "
|
|
1449
|
+
{ status: "upstream_unhealthy", upstream: upstream.status },
|
|
1419
1450
|
{ status: 503 },
|
|
1420
1451
|
);
|
|
1421
1452
|
}
|
|
1422
|
-
|
|
1453
|
+
} catch {
|
|
1454
|
+
return Response.json(
|
|
1455
|
+
{ status: "upstream_unreachable" },
|
|
1456
|
+
{ status: 503 },
|
|
1457
|
+
);
|
|
1423
1458
|
}
|
|
1459
|
+
return Response.json({ status: "ok" });
|
|
1460
|
+
}
|
|
1424
1461
|
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1462
|
+
// Per-request IP resolver — scoped to this request so it remains
|
|
1463
|
+
// correct across async yields under concurrent load.
|
|
1464
|
+
const resolveClientIp: GetClientIp = () =>
|
|
1465
|
+
getClientIp(req, svr, config.trustProxy);
|
|
1429
1466
|
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
// Bun's WS upgrade needs `server.upgrade()` which doesn't return
|
|
1443
|
-
// a Response, so these can't go through the route table.
|
|
1444
|
-
if (url.pathname === "/webhooks/twilio/relay") {
|
|
1445
|
-
const upgradeResult = handleTwilioRelayWs(req, server);
|
|
1446
|
-
if (upgradeResult !== undefined) return upgradeResult;
|
|
1447
|
-
return undefined as unknown as Response;
|
|
1448
|
-
}
|
|
1467
|
+
const rateLimitResponse = checkAuthRateLimit(
|
|
1468
|
+
url,
|
|
1469
|
+
authRateLimiter,
|
|
1470
|
+
resolveClientIp(),
|
|
1471
|
+
);
|
|
1472
|
+
if (rateLimitResponse) {
|
|
1473
|
+
if (extensionOrigin)
|
|
1474
|
+
return withExtensionCorsHeaders(rateLimitResponse, extensionOrigin);
|
|
1475
|
+
if (webviewOrigin)
|
|
1476
|
+
return withCorsHeaders(rateLimitResponse, webviewOrigin);
|
|
1477
|
+
return rateLimitResponse;
|
|
1478
|
+
}
|
|
1449
1479
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1480
|
+
// ── Pre-router: WebSocket upgrades ──
|
|
1481
|
+
// Bun's WS upgrade needs `server.upgrade()` which doesn't return
|
|
1482
|
+
// a Response, so these can't go through the route table.
|
|
1483
|
+
if (url.pathname === TWILIO_RELAY_WEBHOOK_PATH) {
|
|
1484
|
+
const upgradeResult = handleTwilioRelayWs(req, server);
|
|
1485
|
+
if (upgradeResult !== undefined) return upgradeResult;
|
|
1486
|
+
return undefined as unknown as Response;
|
|
1487
|
+
}
|
|
1458
1488
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1489
|
+
if (
|
|
1490
|
+
url.pathname === TWILIO_MEDIA_STREAM_WEBHOOK_PATH ||
|
|
1491
|
+
url.pathname.startsWith(`${TWILIO_MEDIA_STREAM_WEBHOOK_PATH}/`)
|
|
1492
|
+
) {
|
|
1493
|
+
const upgradeResult = handleTwilioMediaWs(req, server);
|
|
1494
|
+
if (upgradeResult !== undefined) return upgradeResult;
|
|
1495
|
+
return undefined as unknown as Response;
|
|
1496
|
+
}
|
|
1464
1497
|
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1498
|
+
if (url.pathname === "/v1/stt/stream") {
|
|
1499
|
+
const upgradeResult = handleSttStreamWs(req, server);
|
|
1500
|
+
if (upgradeResult !== undefined) return upgradeResult;
|
|
1501
|
+
return undefined as unknown as Response;
|
|
1502
|
+
}
|
|
1470
1503
|
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1504
|
+
if (url.pathname === "/v1/live-voice") {
|
|
1505
|
+
const upgradeResult = handleLiveVoiceWs(req, server);
|
|
1506
|
+
if (upgradeResult !== undefined) return upgradeResult;
|
|
1507
|
+
return undefined as unknown as Response;
|
|
1508
|
+
}
|
|
1476
1509
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1510
|
+
// Attach a trace ID to every non-healthcheck request for
|
|
1511
|
+
// end-to-end correlation across webhook -> runtime -> reply.
|
|
1512
|
+
if (!req.headers.has("x-trace-id")) {
|
|
1513
|
+
req.headers.set("x-trace-id", generateTraceId());
|
|
1514
|
+
}
|
|
1482
1515
|
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
}
|
|
1491
|
-
return response;
|
|
1516
|
+
// ── Route table dispatch ──
|
|
1517
|
+
try {
|
|
1518
|
+
const response = router(req, url, resolveClientIp, svr);
|
|
1519
|
+
if (response !== null) {
|
|
1520
|
+
if (extensionOrigin) {
|
|
1521
|
+
const resolved = await response;
|
|
1522
|
+
return withExtensionCorsHeaders(resolved, extensionOrigin);
|
|
1492
1523
|
}
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
// errors during webview requests would otherwise lose CORS headers.
|
|
1497
|
-
if (!webviewOrigin) throw err;
|
|
1498
|
-
if (err instanceof CircuitBreakerOpenError) {
|
|
1499
|
-
return withCorsHeaders(
|
|
1500
|
-
Response.json(
|
|
1501
|
-
{
|
|
1502
|
-
error:
|
|
1503
|
-
"Service temporarily unavailable \u2014 runtime is unreachable",
|
|
1504
|
-
},
|
|
1505
|
-
{
|
|
1506
|
-
status: 503,
|
|
1507
|
-
headers: { "Retry-After": String(err.retryAfterSecs) },
|
|
1508
|
-
},
|
|
1509
|
-
),
|
|
1510
|
-
webviewOrigin,
|
|
1511
|
-
);
|
|
1524
|
+
if (webviewOrigin) {
|
|
1525
|
+
const resolved = await response;
|
|
1526
|
+
return withCorsHeaders(resolved, webviewOrigin);
|
|
1512
1527
|
}
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1528
|
+
return response;
|
|
1529
|
+
}
|
|
1530
|
+
} catch (err) {
|
|
1531
|
+
// Mirror the error() handler logic while retaining CORS context.
|
|
1532
|
+
// Bun's error() callback doesn't receive the request, so thrown
|
|
1533
|
+
// errors during webview/extension requests would otherwise lose CORS headers.
|
|
1534
|
+
if (!webviewOrigin && !extensionOrigin) throw err;
|
|
1535
|
+
if (err instanceof CircuitBreakerOpenError) {
|
|
1536
|
+
const body = Response.json(
|
|
1537
|
+
{
|
|
1538
|
+
error:
|
|
1539
|
+
"Service temporarily unavailable \u2014 runtime is unreachable",
|
|
1540
|
+
},
|
|
1541
|
+
{
|
|
1542
|
+
status: 503,
|
|
1543
|
+
headers: { "Retry-After": String(err.retryAfterSecs) },
|
|
1544
|
+
},
|
|
1517
1545
|
);
|
|
1546
|
+
if (extensionOrigin) return withExtensionCorsHeaders(body, extensionOrigin);
|
|
1547
|
+
return withCorsHeaders(body, webviewOrigin!);
|
|
1518
1548
|
}
|
|
1519
|
-
|
|
1520
|
-
const
|
|
1521
|
-
{ error: "
|
|
1522
|
-
{ status:
|
|
1549
|
+
log.error({ err }, "Unhandled gateway error");
|
|
1550
|
+
const errBody = Response.json(
|
|
1551
|
+
{ error: "Internal server error" },
|
|
1552
|
+
{ status: 500 },
|
|
1523
1553
|
);
|
|
1524
|
-
if (
|
|
1525
|
-
return
|
|
1526
|
-
}
|
|
1527
|
-
});
|
|
1554
|
+
if (extensionOrigin) return withExtensionCorsHeaders(errBody, extensionOrigin);
|
|
1555
|
+
return withCorsHeaders(errBody, webviewOrigin!);
|
|
1556
|
+
}
|
|
1528
1557
|
|
|
1529
|
-
|
|
1558
|
+
const notFound = Response.json(
|
|
1559
|
+
{ error: "Not found", source: "gateway" },
|
|
1560
|
+
{ status: 404 },
|
|
1561
|
+
);
|
|
1562
|
+
if (extensionOrigin) return withExtensionCorsHeaders(notFound, extensionOrigin);
|
|
1563
|
+
if (webviewOrigin) return withCorsHeaders(notFound, webviewOrigin);
|
|
1564
|
+
return notFound;
|
|
1565
|
+
}
|
|
1530
1566
|
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
runPostAssistantReady().catch((err) => {
|
|
1535
|
-
log.error({ err }, "Post-assistant-ready lifecycle failed");
|
|
1536
|
-
});
|
|
1567
|
+
log.info({ port: server.port }, "Gateway HTTP server listening");
|
|
1568
|
+
logAuthBypassState();
|
|
1569
|
+
velayTunnelClient?.start();
|
|
1537
1570
|
|
|
1538
1571
|
// Start periodic background cleanup for dedup caches
|
|
1539
1572
|
telegramDedupCache.startCleanup();
|
|
@@ -1668,6 +1701,20 @@ async function main() {
|
|
|
1668
1701
|
}
|
|
1669
1702
|
|
|
1670
1703
|
const forward = async () => {
|
|
1704
|
+
// Seed contact channel for the Slack actor (dual-write, fire-and-forget).
|
|
1705
|
+
// Covers both DMs (externalChatId = DM channel) and workspace messages.
|
|
1706
|
+
if (normalized.event.actor.actorExternalId) {
|
|
1707
|
+
void upsertContactChannel({
|
|
1708
|
+
sourceChannel: "slack",
|
|
1709
|
+
externalUserId: normalized.event.actor.actorExternalId,
|
|
1710
|
+
...(normalized.event.source.chatType === "im"
|
|
1711
|
+
? { externalChatId: normalized.event.message.conversationExternalId }
|
|
1712
|
+
: {}),
|
|
1713
|
+
displayName: normalized.event.actor.displayName,
|
|
1714
|
+
username: normalized.event.actor.username,
|
|
1715
|
+
}).catch(() => {});
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1671
1718
|
try {
|
|
1672
1719
|
// Download and upload attachments if present (skip for edits and
|
|
1673
1720
|
// callback actions — edits only update text, callbacks have no media)
|
|
@@ -1889,12 +1936,7 @@ async function main() {
|
|
|
1889
1936
|
);
|
|
1890
1937
|
});
|
|
1891
1938
|
|
|
1892
|
-
|
|
1893
|
-
// initial startup). Gated behind channel-avatar-sync feature flag.
|
|
1894
|
-
// Known limitation: if the flag is toggled off without a credential
|
|
1895
|
-
// change, the syncer stays registered until the next credential
|
|
1896
|
-
// change triggers this callback.
|
|
1897
|
-
if (slackReady && getMergedFeatureFlags()["channel-avatar-sync"]) {
|
|
1939
|
+
if (slackReady) {
|
|
1898
1940
|
avatarChannelSyncer.register(new SlackAvatarSyncer(credentialCache));
|
|
1899
1941
|
avatarChannelSyncer.syncToChannel("slack").catch((err) => {
|
|
1900
1942
|
log.warn({ err }, "Initial Slack avatar sync failed");
|
|
@@ -1904,6 +1946,15 @@ async function main() {
|
|
|
1904
1946
|
}
|
|
1905
1947
|
}
|
|
1906
1948
|
|
|
1949
|
+
if (changed.has("twilio")) {
|
|
1950
|
+
syncConfiguredTwilioPhoneNumberWebhooks({
|
|
1951
|
+
credentials: credentialCache,
|
|
1952
|
+
configFile: configFileCache,
|
|
1953
|
+
}).catch((err) => {
|
|
1954
|
+
log.warn({ err }, "Twilio webhook sync failed after credential change");
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1907
1958
|
// Register email callback route with the platform so inbound email
|
|
1908
1959
|
// webhooks are forwarded to this gateway (same pattern as Telegram).
|
|
1909
1960
|
// Fires on initial credential load and whenever vellum credentials change
|
|
@@ -1935,7 +1986,13 @@ async function main() {
|
|
|
1935
1986
|
configFileCache.invalidate();
|
|
1936
1987
|
|
|
1937
1988
|
// Side effect: reconcile Telegram webhook when ingress URL changes
|
|
1938
|
-
|
|
1989
|
+
const onlyVelayPublicBaseUrlChanged = isOnlyVelayPublicBaseUrlChange(event);
|
|
1990
|
+
|
|
1991
|
+
if (
|
|
1992
|
+
event.changedKeys.has("ingress") &&
|
|
1993
|
+
!onlyVelayPublicBaseUrlChanged &&
|
|
1994
|
+
isTelegramConfigured()
|
|
1995
|
+
) {
|
|
1939
1996
|
reconcileTelegramWebhook(telegramCaches).catch((err) => {
|
|
1940
1997
|
log.error(
|
|
1941
1998
|
{ err },
|
|
@@ -1944,9 +2001,22 @@ async function main() {
|
|
|
1944
2001
|
});
|
|
1945
2002
|
}
|
|
1946
2003
|
|
|
2004
|
+
if (shouldSyncTwilioPhoneWebhooksAfterConfigChange(event)) {
|
|
2005
|
+
syncConfiguredTwilioPhoneNumberWebhooks({
|
|
2006
|
+
credentials: credentialCache,
|
|
2007
|
+
configFile: configFileCache,
|
|
2008
|
+
}).catch((err) => {
|
|
2009
|
+
log.warn({ err }, "Twilio webhook sync failed after config change");
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
|
|
1947
2013
|
// Side effect: re-register email callback when ingress URL changes so
|
|
1948
2014
|
// the platform callback route points at the new self-hosted URL.
|
|
1949
|
-
if (
|
|
2015
|
+
if (
|
|
2016
|
+
event.changedKeys.has("ingress") &&
|
|
2017
|
+
!onlyVelayPublicBaseUrlChanged &&
|
|
2018
|
+
vellumReady
|
|
2019
|
+
) {
|
|
1950
2020
|
registerEmailCallbackRoute({
|
|
1951
2021
|
credentials: credentialCache,
|
|
1952
2022
|
configFile: configFileCache,
|
|
@@ -1967,12 +2037,16 @@ async function main() {
|
|
|
1967
2037
|
...contactRoutes,
|
|
1968
2038
|
...thresholdRoutes,
|
|
1969
2039
|
...riskClassificationRoutes,
|
|
1970
|
-
...capabilityTokenRoutes,
|
|
1971
2040
|
]);
|
|
1972
2041
|
ipcServer.start();
|
|
1973
2042
|
|
|
1974
2043
|
void refreshRouteSchema();
|
|
1975
2044
|
|
|
2045
|
+
// ── Backup worker ──
|
|
2046
|
+
const backupWorkerHandle = startBackupWorker({
|
|
2047
|
+
assistantRuntimeBaseUrl: config.assistantRuntimeBaseUrl,
|
|
2048
|
+
});
|
|
2049
|
+
|
|
1976
2050
|
const featureFlagWatcher = new FeatureFlagWatcher();
|
|
1977
2051
|
featureFlagWatcher.start();
|
|
1978
2052
|
|
|
@@ -2016,25 +2090,30 @@ async function main() {
|
|
|
2016
2090
|
process.on("SIGTERM", () => {
|
|
2017
2091
|
log.info("SIGTERM received, starting graceful shutdown");
|
|
2018
2092
|
draining = true;
|
|
2093
|
+
const shutdownTasks: Promise<void>[] = [];
|
|
2019
2094
|
sleepWakeDetector.stop();
|
|
2095
|
+
backupWorkerHandle.stop();
|
|
2020
2096
|
credentialWatcher.stop();
|
|
2021
2097
|
configFileWatcher.stop();
|
|
2022
2098
|
avatarSyncWatcher.stop();
|
|
2023
2099
|
featureFlagWatcher.stop();
|
|
2024
2100
|
remoteFeatureFlagSync.stop();
|
|
2101
|
+
const velayStop = velayTunnelClient?.stop();
|
|
2102
|
+
if (velayStop) shutdownTasks.push(velayStop);
|
|
2025
2103
|
ipcServer.stop();
|
|
2026
2104
|
telegramDedupCache.stopCleanup();
|
|
2027
2105
|
whatsappDedupCache.stopCleanup();
|
|
2028
2106
|
emailDedupCache.stopCleanup();
|
|
2029
|
-
pairingStore.stop();
|
|
2030
2107
|
if (slackSocketClient) {
|
|
2031
2108
|
slackSocketClient.stop();
|
|
2032
2109
|
slackSocketClient = null;
|
|
2033
2110
|
}
|
|
2034
2111
|
setTimeout(() => {
|
|
2035
2112
|
log.info("Drain window elapsed, stopping server");
|
|
2036
|
-
|
|
2037
|
-
|
|
2113
|
+
void Promise.allSettled(shutdownTasks).then(() => {
|
|
2114
|
+
server.stop(true);
|
|
2115
|
+
process.exit(0);
|
|
2116
|
+
});
|
|
2038
2117
|
}, drainMs);
|
|
2039
2118
|
});
|
|
2040
2119
|
}
|