@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
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import type { OutgoingHttpHeaders } from "node:http";
|
|
3
|
+
import { buildUpstreamUrl, stripHopByHop } from "@vellumai/assistant-client";
|
|
4
|
+
|
|
5
|
+
import type { VelayHeaders } from "./protocol.js";
|
|
6
|
+
|
|
7
|
+
const MAX_WEBSOCKET_CLOSE_REASON_BYTES = 123;
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const VELAY_ALLOWED_HTTP_PATH_PREFIXES = ["/webhooks/twilio/"] as const;
|
|
11
|
+
|
|
12
|
+
export function isAllowedVelayHttpPath(path: string): boolean {
|
|
13
|
+
return VELAY_ALLOWED_HTTP_PATH_PREFIXES.some((prefix) =>
|
|
14
|
+
path.startsWith(prefix),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isSafeOriginRelativePath(path: string): boolean {
|
|
19
|
+
if (!path.startsWith("/") || path.startsWith("//")) return false;
|
|
20
|
+
if (path.includes("\\") || path.includes("?") || path.includes("#")) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const parsed = new URL(path, "http://127.0.0.1");
|
|
25
|
+
return parsed.origin === "http://127.0.0.1" && parsed.pathname === path;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function formatRawQuery(rawQuery: string | undefined): string {
|
|
32
|
+
if (!rawQuery) return "";
|
|
33
|
+
return `?${rawQuery.replace(/^\?/, "")}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function buildLoopbackHttpUrl(
|
|
37
|
+
gatewayLoopbackBaseUrl: string,
|
|
38
|
+
path: string,
|
|
39
|
+
rawQuery?: string,
|
|
40
|
+
): string | undefined {
|
|
41
|
+
if (!isSafeOriginRelativePath(path) || !isAllowedVelayHttpPath(path)) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return buildUpstreamUrl(
|
|
45
|
+
gatewayLoopbackBaseUrl,
|
|
46
|
+
path,
|
|
47
|
+
formatRawQuery(rawQuery),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildLoopbackWebSocketUrl(
|
|
52
|
+
gatewayLoopbackBaseUrl: string,
|
|
53
|
+
path: string,
|
|
54
|
+
rawQuery?: string,
|
|
55
|
+
): string | undefined {
|
|
56
|
+
const httpUrl = buildLoopbackHttpUrl(gatewayLoopbackBaseUrl, path, rawQuery);
|
|
57
|
+
if (!httpUrl) return undefined;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const url = new URL(httpUrl);
|
|
61
|
+
if (url.protocol === "http:") url.protocol = "ws:";
|
|
62
|
+
if (url.protocol === "https:") url.protocol = "wss:";
|
|
63
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") return undefined;
|
|
64
|
+
return url.toString();
|
|
65
|
+
} catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function headersToWeb(headers: VelayHeaders): Headers {
|
|
71
|
+
const webHeaders = new Headers();
|
|
72
|
+
for (const [name, values] of Object.entries(headers)) {
|
|
73
|
+
for (const value of values) {
|
|
74
|
+
webHeaders.append(name, value);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return webHeaders;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function headersToVelay(headers: Headers): VelayHeaders {
|
|
81
|
+
const velayHeaders: VelayHeaders = {};
|
|
82
|
+
for (const [name, value] of headers.entries()) {
|
|
83
|
+
velayHeaders[name] ??= [];
|
|
84
|
+
velayHeaders[name].push(value);
|
|
85
|
+
}
|
|
86
|
+
return velayHeaders;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function headersFromVelay(headers: VelayHeaders): Headers {
|
|
90
|
+
return stripHopByHop(headersToWeb(headers));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function websocketHeadersFromVelay(
|
|
94
|
+
headers: VelayHeaders,
|
|
95
|
+
): OutgoingHttpHeaders {
|
|
96
|
+
const cleaned = headersFromVelay(headers);
|
|
97
|
+
const outgoing: OutgoingHttpHeaders = {};
|
|
98
|
+
|
|
99
|
+
for (const [name, value] of cleaned.entries()) {
|
|
100
|
+
if (name.startsWith("sec-websocket-")) continue;
|
|
101
|
+
outgoing[name] = value;
|
|
102
|
+
}
|
|
103
|
+
return outgoing;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function isBase64(value: string): boolean {
|
|
107
|
+
return /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(
|
|
108
|
+
value,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function decodeBase64Bytes(value: string): Uint8Array | undefined {
|
|
113
|
+
if (!isBase64(value)) return undefined;
|
|
114
|
+
return new Uint8Array(Buffer.from(value, "base64"));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function decodeOptionalBase64ArrayBuffer(
|
|
118
|
+
value: string | undefined,
|
|
119
|
+
): { ok: true; value?: ArrayBuffer } | { ok: false } {
|
|
120
|
+
if (!value) return { ok: true };
|
|
121
|
+
|
|
122
|
+
const bytes = decodeBase64Bytes(value);
|
|
123
|
+
if (bytes === undefined) return { ok: false };
|
|
124
|
+
return { ok: true, value: bytesToArrayBuffer(bytes) };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function encodeBase64(value: string | ArrayBuffer | Uint8Array): string {
|
|
128
|
+
if (typeof value === "string") return Buffer.from(value).toString("base64");
|
|
129
|
+
if (value instanceof ArrayBuffer) {
|
|
130
|
+
return Buffer.from(value).toString("base64");
|
|
131
|
+
}
|
|
132
|
+
return Buffer.from(value).toString("base64");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function binaryLikeToBytes(data: unknown): Promise<Uint8Array> {
|
|
136
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
137
|
+
if (ArrayBuffer.isView(data)) {
|
|
138
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
139
|
+
}
|
|
140
|
+
if (data instanceof Blob) return new Uint8Array(await data.arrayBuffer());
|
|
141
|
+
return Buffer.from(String(data));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function closeWebSocket(
|
|
145
|
+
ws: WebSocket,
|
|
146
|
+
code?: number,
|
|
147
|
+
reason?: string,
|
|
148
|
+
): void {
|
|
149
|
+
if (
|
|
150
|
+
ws.readyState !== WebSocket.OPEN &&
|
|
151
|
+
ws.readyState !== WebSocket.CONNECTING
|
|
152
|
+
) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const closeArgs = sanitizeWebSocketCloseArgs(code, reason);
|
|
157
|
+
try {
|
|
158
|
+
if (!closeArgs) {
|
|
159
|
+
ws.close();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
ws.close(closeArgs.code, closeArgs.reason);
|
|
163
|
+
} catch {
|
|
164
|
+
try {
|
|
165
|
+
ws.close();
|
|
166
|
+
} catch {
|
|
167
|
+
// The socket is already closing or the runtime rejected the close call.
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function sanitizeWebSocketCloseArgs(
|
|
173
|
+
code?: number,
|
|
174
|
+
reason?: string,
|
|
175
|
+
): { code: number; reason?: string } | undefined {
|
|
176
|
+
const safeCode = toSafeWebSocketCloseCode(code);
|
|
177
|
+
if (safeCode === undefined) return undefined;
|
|
178
|
+
|
|
179
|
+
const sanitizedReason =
|
|
180
|
+
typeof reason === "string" ? truncateCloseReason(reason) : undefined;
|
|
181
|
+
return sanitizedReason === undefined
|
|
182
|
+
? { code: safeCode }
|
|
183
|
+
: { code: safeCode, reason: sanitizedReason };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function toSafeWebSocketCloseCode(
|
|
187
|
+
code: number | undefined,
|
|
188
|
+
): number | undefined {
|
|
189
|
+
if (typeof code !== "number" || !Number.isInteger(code)) return undefined;
|
|
190
|
+
if (code === 1000) return code;
|
|
191
|
+
if (code >= 3000 && code <= 4999) return code;
|
|
192
|
+
// WebSocket close events can report these RFC 6455 codes, but the
|
|
193
|
+
// JavaScript close() API only portably accepts 1000 or 3000-4999.
|
|
194
|
+
if (isRemappableProtocolCloseCode(code)) return 3000 + code;
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function isRemappableProtocolCloseCode(code: number): boolean {
|
|
199
|
+
return (
|
|
200
|
+
code === 1001 ||
|
|
201
|
+
code === 1002 ||
|
|
202
|
+
code === 1003 ||
|
|
203
|
+
(code >= 1007 && code <= 1014)
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function truncateCloseReason(reason: string): string {
|
|
208
|
+
const encoder = new TextEncoder();
|
|
209
|
+
if (encoder.encode(reason).byteLength <= MAX_WEBSOCKET_CLOSE_REASON_BYTES) {
|
|
210
|
+
return reason;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let truncated = "";
|
|
214
|
+
let byteLength = 0;
|
|
215
|
+
for (const character of reason) {
|
|
216
|
+
const characterLength = encoder.encode(character).byteLength;
|
|
217
|
+
if (byteLength + characterLength > MAX_WEBSOCKET_CLOSE_REASON_BYTES) break;
|
|
218
|
+
truncated += character;
|
|
219
|
+
byteLength += characterLength;
|
|
220
|
+
}
|
|
221
|
+
return truncated;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function bytesToArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
225
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
226
|
+
copy.set(bytes);
|
|
227
|
+
return copy.buffer as ArrayBuffer;
|
|
228
|
+
}
|