@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
|
@@ -1,698 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, mock, beforeEach, afterAll } from "bun:test";
|
|
2
|
-
import type { GatewayConfig } from "../config.js";
|
|
3
|
-
import { initSigningKey, mintToken } from "../auth/token-service.js";
|
|
4
|
-
import { CURRENT_POLICY_EPOCH } from "../auth/policy.js";
|
|
5
|
-
import {
|
|
6
|
-
checkBrowserRelayAuth,
|
|
7
|
-
createBrowserRelayWebsocketHandler,
|
|
8
|
-
getBrowserRelayWebsocketHandlers,
|
|
9
|
-
isLoopbackPeer,
|
|
10
|
-
} from "../http/routes/browser-relay-websocket.js";
|
|
11
|
-
|
|
12
|
-
const TEST_SIGNING_KEY = Buffer.from("test-signing-key-at-least-32-bytes-long");
|
|
13
|
-
initSigningKey(TEST_SIGNING_KEY);
|
|
14
|
-
|
|
15
|
-
const TEST_ACTOR_PRINCIPAL = "guardian-actor-123";
|
|
16
|
-
|
|
17
|
-
/** Mint a valid actor edge JWT for browser relay auth. */
|
|
18
|
-
function mintEdgeToken(actorPrincipalId: string = "test-user"): string {
|
|
19
|
-
return mintToken({
|
|
20
|
-
aud: "vellum-gateway",
|
|
21
|
-
sub: `actor:test-assistant:${actorPrincipalId}`,
|
|
22
|
-
scope_profile: "actor_client_v1",
|
|
23
|
-
policy_epoch: CURRENT_POLICY_EPOCH,
|
|
24
|
-
ttlSeconds: 300,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Mint a service-style browser-relay edge token (svc:browser-relay:self).
|
|
30
|
-
* This token is valid for the gateway audience but carries no actor
|
|
31
|
-
* principal in its sub claim.
|
|
32
|
-
*/
|
|
33
|
-
function mintServiceEdgeToken(): string {
|
|
34
|
-
return mintToken({
|
|
35
|
-
aud: "vellum-gateway",
|
|
36
|
-
sub: "svc:browser-relay:self",
|
|
37
|
-
scope_profile: "gateway_service_v1",
|
|
38
|
-
policy_epoch: CURRENT_POLICY_EPOCH,
|
|
39
|
-
ttlSeconds: 300,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const WS_CONNECTING = WebSocket.CONNECTING; // 0
|
|
44
|
-
const WS_OPEN = WebSocket.OPEN; // 1
|
|
45
|
-
const WS_CLOSED = WebSocket.CLOSED; // 3
|
|
46
|
-
|
|
47
|
-
function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
48
|
-
const merged: GatewayConfig = {
|
|
49
|
-
assistantRuntimeBaseUrl: "http://localhost:7821",
|
|
50
|
-
routingEntries: [],
|
|
51
|
-
defaultAssistantId: undefined,
|
|
52
|
-
unmappedPolicy: "reject",
|
|
53
|
-
port: 7830,
|
|
54
|
-
runtimeProxyEnabled: false,
|
|
55
|
-
runtimeProxyRequireAuth: true,
|
|
56
|
-
shutdownDrainMs: 5000,
|
|
57
|
-
runtimeTimeoutMs: 30000,
|
|
58
|
-
runtimeMaxRetries: 2,
|
|
59
|
-
runtimeInitialBackoffMs: 500,
|
|
60
|
-
maxWebhookPayloadBytes: 1048576,
|
|
61
|
-
logFile: { dir: undefined, retentionDays: 30 },
|
|
62
|
-
maxAttachmentBytes: {
|
|
63
|
-
telegram: 50 * 1024 * 1024,
|
|
64
|
-
slack: 100 * 1024 * 1024,
|
|
65
|
-
whatsapp: 16 * 1024 * 1024,
|
|
66
|
-
default: 50 * 1024 * 1024,
|
|
67
|
-
},
|
|
68
|
-
maxAttachmentConcurrency: 3,
|
|
69
|
-
gatewayInternalBaseUrl: "http://127.0.0.1:7830",
|
|
70
|
-
trustProxy: false,
|
|
71
|
-
...overrides,
|
|
72
|
-
};
|
|
73
|
-
return merged;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function createFakeDownstreamWs(data: Record<string, unknown> = {}) {
|
|
77
|
-
const sent: (string | Uint8Array)[] = [];
|
|
78
|
-
const closes: { code: number; reason: string }[] = [];
|
|
79
|
-
return {
|
|
80
|
-
data,
|
|
81
|
-
sent,
|
|
82
|
-
closes,
|
|
83
|
-
send: mock((msg: string | Uint8Array) => {
|
|
84
|
-
sent.push(msg);
|
|
85
|
-
}),
|
|
86
|
-
close: mock((code?: number, reason?: string) => {
|
|
87
|
-
closes.push({ code: code ?? 1000, reason: reason ?? "" });
|
|
88
|
-
}),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function createFakeUpstreamWs() {
|
|
93
|
-
const listeners: Record<string, ((...args: unknown[]) => void)[]> = {};
|
|
94
|
-
const sent: unknown[] = [];
|
|
95
|
-
return {
|
|
96
|
-
readyState: WS_CONNECTING as number,
|
|
97
|
-
sent,
|
|
98
|
-
listeners,
|
|
99
|
-
addEventListener: mock(
|
|
100
|
-
(event: string, cb: (...args: unknown[]) => void) => {
|
|
101
|
-
(listeners[event] ??= []).push(cb);
|
|
102
|
-
},
|
|
103
|
-
),
|
|
104
|
-
send: mock((msg: unknown) => {
|
|
105
|
-
sent.push(msg);
|
|
106
|
-
}),
|
|
107
|
-
close: mock(() => {}),
|
|
108
|
-
emit(event: string, detail: unknown = {}) {
|
|
109
|
-
for (const cb of listeners[event] ?? []) {
|
|
110
|
-
cb(detail);
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
describe("createBrowserRelayWebsocketHandler", () => {
|
|
117
|
-
const TEST_TOKEN = mintEdgeToken();
|
|
118
|
-
|
|
119
|
-
test("upgrades when token query parameter is valid", () => {
|
|
120
|
-
const config = makeConfig({});
|
|
121
|
-
const handler = createBrowserRelayWebsocketHandler(config);
|
|
122
|
-
const req = new Request(
|
|
123
|
-
`http://localhost:7830/v1/browser-relay?token=${TEST_TOKEN}`,
|
|
124
|
-
{ headers: { upgrade: "websocket" } },
|
|
125
|
-
);
|
|
126
|
-
const fakeServer = {
|
|
127
|
-
requestIP: mock(() => ({
|
|
128
|
-
address: "127.0.0.1",
|
|
129
|
-
family: "IPv4",
|
|
130
|
-
port: 54000,
|
|
131
|
-
})),
|
|
132
|
-
upgrade: mock(() => true),
|
|
133
|
-
} as unknown as import("bun").Server<any>;
|
|
134
|
-
const res = handler(req, fakeServer);
|
|
135
|
-
|
|
136
|
-
expect(res).toBeUndefined();
|
|
137
|
-
expect(fakeServer.upgrade).toHaveBeenCalledTimes(1);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("returns 401 when token is missing", () => {
|
|
141
|
-
const config = makeConfig({});
|
|
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(() => ({
|
|
148
|
-
address: "127.0.0.1",
|
|
149
|
-
family: "IPv4",
|
|
150
|
-
port: 54000,
|
|
151
|
-
})),
|
|
152
|
-
upgrade: mock(() => true),
|
|
153
|
-
} as unknown as import("bun").Server<any>;
|
|
154
|
-
const res = handler(req, fakeServer);
|
|
155
|
-
|
|
156
|
-
expect(res).toBeInstanceOf(Response);
|
|
157
|
-
expect(res!.status).toBe(401);
|
|
158
|
-
expect(fakeServer.upgrade).not.toHaveBeenCalled();
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test("allows unauthenticated upgrade when runtime proxy auth is disabled", () => {
|
|
162
|
-
const config = makeConfig({ runtimeProxyRequireAuth: false });
|
|
163
|
-
const handler = createBrowserRelayWebsocketHandler(config);
|
|
164
|
-
const req = new Request("http://localhost:7830/v1/browser-relay", {
|
|
165
|
-
headers: { upgrade: "websocket" },
|
|
166
|
-
});
|
|
167
|
-
const fakeServer = {
|
|
168
|
-
requestIP: mock(() => ({
|
|
169
|
-
address: "127.0.0.1",
|
|
170
|
-
family: "IPv4",
|
|
171
|
-
port: 54000,
|
|
172
|
-
})),
|
|
173
|
-
upgrade: mock(() => true),
|
|
174
|
-
} as unknown as import("bun").Server<any>;
|
|
175
|
-
const res = handler(req, fakeServer);
|
|
176
|
-
|
|
177
|
-
expect(res).toBeUndefined();
|
|
178
|
-
expect(fakeServer.upgrade).toHaveBeenCalledTimes(1);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("returns 403 when non-loopback host is requested from a public peer", () => {
|
|
182
|
-
const config = makeConfig({});
|
|
183
|
-
const handler = createBrowserRelayWebsocketHandler(config);
|
|
184
|
-
const req = new Request(
|
|
185
|
-
`http://gateway.example.com:7830/v1/browser-relay?token=${TEST_TOKEN}`,
|
|
186
|
-
{ headers: { upgrade: "websocket" } },
|
|
187
|
-
);
|
|
188
|
-
const fakeServer = {
|
|
189
|
-
requestIP: mock(() => ({
|
|
190
|
-
address: "8.8.8.8",
|
|
191
|
-
family: "IPv4",
|
|
192
|
-
port: 54000,
|
|
193
|
-
})),
|
|
194
|
-
upgrade: mock(() => true),
|
|
195
|
-
} as unknown as import("bun").Server<any>;
|
|
196
|
-
|
|
197
|
-
const res = handler(req, fakeServer);
|
|
198
|
-
|
|
199
|
-
expect(res).toBeInstanceOf(Response);
|
|
200
|
-
expect(res!.status).toBe(403);
|
|
201
|
-
expect(fakeServer.upgrade).not.toHaveBeenCalled();
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test("returns 403 for localhost host when peer is public (host spoof prevention)", () => {
|
|
205
|
-
const config = makeConfig({});
|
|
206
|
-
const handler = createBrowserRelayWebsocketHandler(config);
|
|
207
|
-
const req = new Request(
|
|
208
|
-
`http://localhost:7830/v1/browser-relay?token=${TEST_TOKEN}`,
|
|
209
|
-
{ headers: { upgrade: "websocket" } },
|
|
210
|
-
);
|
|
211
|
-
const fakeServer = {
|
|
212
|
-
requestIP: mock(() => ({
|
|
213
|
-
address: "8.8.8.8",
|
|
214
|
-
family: "IPv4",
|
|
215
|
-
port: 54000,
|
|
216
|
-
})),
|
|
217
|
-
upgrade: mock(() => true),
|
|
218
|
-
} as unknown as import("bun").Server<any>;
|
|
219
|
-
|
|
220
|
-
const res = handler(req, fakeServer);
|
|
221
|
-
|
|
222
|
-
expect(res).toBeInstanceOf(Response);
|
|
223
|
-
expect(res!.status).toBe(403);
|
|
224
|
-
expect(fakeServer.upgrade).not.toHaveBeenCalled();
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
test("allows non-loopback host when peer is private network", () => {
|
|
228
|
-
const config = makeConfig({});
|
|
229
|
-
const handler = createBrowserRelayWebsocketHandler(config);
|
|
230
|
-
const req = new Request(
|
|
231
|
-
`http://gateway.example.com:7830/v1/browser-relay?token=${TEST_TOKEN}`,
|
|
232
|
-
{ headers: { upgrade: "websocket" } },
|
|
233
|
-
);
|
|
234
|
-
const fakeServer = {
|
|
235
|
-
requestIP: mock(() => ({
|
|
236
|
-
address: "10.42.0.8",
|
|
237
|
-
family: "IPv4",
|
|
238
|
-
port: 54000,
|
|
239
|
-
})),
|
|
240
|
-
upgrade: mock(() => true),
|
|
241
|
-
} as unknown as import("bun").Server<any>;
|
|
242
|
-
|
|
243
|
-
const res = handler(req, fakeServer);
|
|
244
|
-
|
|
245
|
-
expect(res).toBeUndefined();
|
|
246
|
-
expect(fakeServer.upgrade).toHaveBeenCalledTimes(1);
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
describe("getBrowserRelayWebsocketHandlers", () => {
|
|
251
|
-
const OriginalWebSocket = globalThis.WebSocket;
|
|
252
|
-
let fakeUpstream: ReturnType<typeof createFakeUpstreamWs>;
|
|
253
|
-
let handlers: ReturnType<typeof getBrowserRelayWebsocketHandlers>;
|
|
254
|
-
|
|
255
|
-
beforeEach(() => {
|
|
256
|
-
fakeUpstream = createFakeUpstreamWs();
|
|
257
|
-
const MockWS = mock(() => fakeUpstream);
|
|
258
|
-
Object.assign(MockWS, {
|
|
259
|
-
CONNECTING: WS_CONNECTING,
|
|
260
|
-
OPEN: WS_OPEN,
|
|
261
|
-
CLOSING: 2,
|
|
262
|
-
CLOSED: WS_CLOSED,
|
|
263
|
-
});
|
|
264
|
-
globalThis.WebSocket = MockWS as unknown as typeof WebSocket;
|
|
265
|
-
handlers = getBrowserRelayWebsocketHandlers();
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
afterAll(() => {
|
|
269
|
-
globalThis.WebSocket = OriginalWebSocket;
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
test("open targets runtime browser-relay websocket and flushes buffered messages", () => {
|
|
273
|
-
const ws = createFakeDownstreamWs({
|
|
274
|
-
wsType: "browser-relay",
|
|
275
|
-
config: makeConfig({
|
|
276
|
-
assistantRuntimeBaseUrl: "http://runtime.internal:7821",
|
|
277
|
-
}),
|
|
278
|
-
auth: { authenticated: false, authBypassed: true },
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
handlers.open(ws as never);
|
|
282
|
-
handlers.message(ws as never, "hello-before-open");
|
|
283
|
-
|
|
284
|
-
const MockWS = globalThis.WebSocket as unknown as ReturnType<typeof mock>;
|
|
285
|
-
const calledUrl = (MockWS.mock.calls[0] as unknown[])[0] as string;
|
|
286
|
-
expect(calledUrl).toMatch(
|
|
287
|
-
/^ws:\/\/runtime\.internal:7821\/v1\/browser-relay\?token=ey/,
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
fakeUpstream.readyState = WS_OPEN;
|
|
291
|
-
fakeUpstream.emit("open");
|
|
292
|
-
expect(fakeUpstream.sent).toEqual(["hello-before-open"]);
|
|
293
|
-
|
|
294
|
-
fakeUpstream.emit("message", { data: "runtime-message" });
|
|
295
|
-
expect(ws.sent).toEqual(["runtime-message"]);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
test("open appends guardianId query param when auth context carries one", () => {
|
|
299
|
-
const ws = createFakeDownstreamWs({
|
|
300
|
-
wsType: "browser-relay",
|
|
301
|
-
config: makeConfig({
|
|
302
|
-
assistantRuntimeBaseUrl: "http://runtime.internal:7821",
|
|
303
|
-
}),
|
|
304
|
-
auth: {
|
|
305
|
-
authenticated: true,
|
|
306
|
-
authBypassed: false,
|
|
307
|
-
guardianId: TEST_ACTOR_PRINCIPAL,
|
|
308
|
-
},
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
handlers.open(ws as never);
|
|
312
|
-
|
|
313
|
-
const MockWS = globalThis.WebSocket as unknown as ReturnType<typeof mock>;
|
|
314
|
-
const calledUrl = (MockWS.mock.calls[0] as unknown[])[0] as string;
|
|
315
|
-
const parsed = new URL(calledUrl);
|
|
316
|
-
expect(parsed.protocol).toBe("ws:");
|
|
317
|
-
expect(parsed.host).toBe("runtime.internal:7821");
|
|
318
|
-
expect(parsed.pathname).toBe("/v1/browser-relay");
|
|
319
|
-
expect(parsed.searchParams.get("token")).toMatch(/^ey/);
|
|
320
|
-
expect(parsed.searchParams.get("guardianId")).toBe(TEST_ACTOR_PRINCIPAL);
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
test("open omits guardianId query param when auth context has none (auth-bypass context)", () => {
|
|
324
|
-
const ws = createFakeDownstreamWs({
|
|
325
|
-
wsType: "browser-relay",
|
|
326
|
-
config: makeConfig({
|
|
327
|
-
assistantRuntimeBaseUrl: "http://runtime.internal:7821",
|
|
328
|
-
}),
|
|
329
|
-
auth: { authenticated: true, authBypassed: false },
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
handlers.open(ws as never);
|
|
333
|
-
|
|
334
|
-
const MockWS = globalThis.WebSocket as unknown as ReturnType<typeof mock>;
|
|
335
|
-
const calledUrl = (MockWS.mock.calls[0] as unknown[])[0] as string;
|
|
336
|
-
const parsed = new URL(calledUrl);
|
|
337
|
-
// Auth-bypass contexts may omit guardianId; open() should not
|
|
338
|
-
// synthesize one.
|
|
339
|
-
expect(parsed.searchParams.has("guardianId")).toBe(false);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
test("open appends clientInstanceId query param when auth context carries one", () => {
|
|
343
|
-
const ws = createFakeDownstreamWs({
|
|
344
|
-
wsType: "browser-relay",
|
|
345
|
-
config: makeConfig({
|
|
346
|
-
assistantRuntimeBaseUrl: "http://runtime.internal:7821",
|
|
347
|
-
}),
|
|
348
|
-
auth: {
|
|
349
|
-
authenticated: true,
|
|
350
|
-
authBypassed: false,
|
|
351
|
-
guardianId: TEST_ACTOR_PRINCIPAL,
|
|
352
|
-
clientInstanceId: "install-ABC-123",
|
|
353
|
-
},
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
handlers.open(ws as never);
|
|
357
|
-
|
|
358
|
-
const MockWS = globalThis.WebSocket as unknown as ReturnType<typeof mock>;
|
|
359
|
-
const calledUrl = (MockWS.mock.calls[0] as unknown[])[0] as string;
|
|
360
|
-
const parsed = new URL(calledUrl);
|
|
361
|
-
expect(parsed.pathname).toBe("/v1/browser-relay");
|
|
362
|
-
// Both guardianId and clientInstanceId should ride alongside the
|
|
363
|
-
// upstream service token, matching the runtime's accepted query
|
|
364
|
-
// param names.
|
|
365
|
-
expect(parsed.searchParams.get("guardianId")).toBe(TEST_ACTOR_PRINCIPAL);
|
|
366
|
-
expect(parsed.searchParams.get("clientInstanceId")).toBe("install-ABC-123");
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
test("open forwards clientInstanceId even without a guardianId", () => {
|
|
370
|
-
// Auth-bypass paths can still carry a
|
|
371
|
-
// clientInstanceId lifted from the downstream handshake. The
|
|
372
|
-
// gateway must propagate it so the runtime's multi-instance
|
|
373
|
-
// registry keys the connection correctly.
|
|
374
|
-
const ws = createFakeDownstreamWs({
|
|
375
|
-
wsType: "browser-relay",
|
|
376
|
-
config: makeConfig({
|
|
377
|
-
assistantRuntimeBaseUrl: "http://runtime.internal:7821",
|
|
378
|
-
}),
|
|
379
|
-
auth: {
|
|
380
|
-
authenticated: false,
|
|
381
|
-
authBypassed: true,
|
|
382
|
-
clientInstanceId: "install-XYZ-789",
|
|
383
|
-
},
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
handlers.open(ws as never);
|
|
387
|
-
|
|
388
|
-
const MockWS = globalThis.WebSocket as unknown as ReturnType<typeof mock>;
|
|
389
|
-
const calledUrl = (MockWS.mock.calls[0] as unknown[])[0] as string;
|
|
390
|
-
const parsed = new URL(calledUrl);
|
|
391
|
-
expect(parsed.searchParams.has("guardianId")).toBe(false);
|
|
392
|
-
expect(parsed.searchParams.get("clientInstanceId")).toBe("install-XYZ-789");
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
test("open omits clientInstanceId query param when auth context has none", () => {
|
|
396
|
-
const ws = createFakeDownstreamWs({
|
|
397
|
-
wsType: "browser-relay",
|
|
398
|
-
config: makeConfig({
|
|
399
|
-
assistantRuntimeBaseUrl: "http://runtime.internal:7821",
|
|
400
|
-
}),
|
|
401
|
-
auth: {
|
|
402
|
-
authenticated: true,
|
|
403
|
-
authBypassed: false,
|
|
404
|
-
guardianId: TEST_ACTOR_PRINCIPAL,
|
|
405
|
-
},
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
handlers.open(ws as never);
|
|
409
|
-
|
|
410
|
-
const MockWS = globalThis.WebSocket as unknown as ReturnType<typeof mock>;
|
|
411
|
-
const calledUrl = (MockWS.mock.calls[0] as unknown[])[0] as string;
|
|
412
|
-
const parsed = new URL(calledUrl);
|
|
413
|
-
// Older extension builds (or dev bypass paths) do not emit a
|
|
414
|
-
// clientInstanceId — the gateway must not synthesize one or send
|
|
415
|
-
// an empty string, both of which would break the runtime's
|
|
416
|
-
// legacy-key fallback semantics.
|
|
417
|
-
expect(parsed.searchParams.has("clientInstanceId")).toBe(false);
|
|
418
|
-
});
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
describe("checkBrowserRelayAuth", () => {
|
|
422
|
-
test("returns structured auth context with guardianId for actor edge tokens", () => {
|
|
423
|
-
const token = mintEdgeToken(TEST_ACTOR_PRINCIPAL);
|
|
424
|
-
const config = makeConfig({});
|
|
425
|
-
const req = new Request(
|
|
426
|
-
`http://localhost:7830/v1/browser-relay?token=${token}`,
|
|
427
|
-
{ headers: { upgrade: "websocket" } },
|
|
428
|
-
);
|
|
429
|
-
const url = new URL(req.url);
|
|
430
|
-
|
|
431
|
-
const result = checkBrowserRelayAuth(req, url, config);
|
|
432
|
-
|
|
433
|
-
expect(result.ok).toBe(true);
|
|
434
|
-
if (!result.ok) throw new Error("expected ok");
|
|
435
|
-
expect(result.context.authenticated).toBe(true);
|
|
436
|
-
expect(result.context.authBypassed).toBe(false);
|
|
437
|
-
expect(result.context.guardianId).toBe(TEST_ACTOR_PRINCIPAL);
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
test("rejects service-style edge tokens with no actor principal", () => {
|
|
441
|
-
const token = mintServiceEdgeToken();
|
|
442
|
-
const config = makeConfig({});
|
|
443
|
-
const req = new Request(
|
|
444
|
-
`http://localhost:7830/v1/browser-relay?token=${token}`,
|
|
445
|
-
{ headers: { upgrade: "websocket" } },
|
|
446
|
-
);
|
|
447
|
-
const url = new URL(req.url);
|
|
448
|
-
|
|
449
|
-
const result = checkBrowserRelayAuth(req, url, config);
|
|
450
|
-
|
|
451
|
-
expect(result.ok).toBe(false);
|
|
452
|
-
if (result.ok) throw new Error("expected error");
|
|
453
|
-
expect(result.response.status).toBe(401);
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
test("returns error response when token is missing and auth is required", () => {
|
|
457
|
-
const config = makeConfig({});
|
|
458
|
-
const req = new Request("http://localhost:7830/v1/browser-relay", {
|
|
459
|
-
headers: { upgrade: "websocket" },
|
|
460
|
-
});
|
|
461
|
-
const url = new URL(req.url);
|
|
462
|
-
|
|
463
|
-
const result = checkBrowserRelayAuth(req, url, config);
|
|
464
|
-
|
|
465
|
-
expect(result.ok).toBe(false);
|
|
466
|
-
if (result.ok) throw new Error("expected error");
|
|
467
|
-
expect(result.response.status).toBe(401);
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
test("returns bypassed context when runtimeProxyRequireAuth is disabled", () => {
|
|
471
|
-
const config = makeConfig({ runtimeProxyRequireAuth: false });
|
|
472
|
-
const req = new Request("http://localhost:7830/v1/browser-relay", {
|
|
473
|
-
headers: { upgrade: "websocket" },
|
|
474
|
-
});
|
|
475
|
-
const url = new URL(req.url);
|
|
476
|
-
|
|
477
|
-
const result = checkBrowserRelayAuth(req, url, config);
|
|
478
|
-
|
|
479
|
-
expect(result.ok).toBe(true);
|
|
480
|
-
if (!result.ok) throw new Error("expected ok");
|
|
481
|
-
expect(result.context.authBypassed).toBe(true);
|
|
482
|
-
expect(result.context.authenticated).toBe(false);
|
|
483
|
-
expect(result.context.guardianId).toBeUndefined();
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
test("lifts clientInstanceId from the query param on the downstream handshake", () => {
|
|
487
|
-
// Primary path: the Chrome extension sets clientInstanceId as a
|
|
488
|
-
// query param via `buildUrl`. The gateway must lift it off the
|
|
489
|
-
// handshake and expose it on the auth context so the open()
|
|
490
|
-
// handler can forward it to the runtime.
|
|
491
|
-
const token = mintEdgeToken(TEST_ACTOR_PRINCIPAL);
|
|
492
|
-
const config = makeConfig({});
|
|
493
|
-
const req = new Request(
|
|
494
|
-
`http://localhost:7830/v1/browser-relay?token=${token}&clientInstanceId=install-QUERY`,
|
|
495
|
-
{ headers: { upgrade: "websocket" } },
|
|
496
|
-
);
|
|
497
|
-
const url = new URL(req.url);
|
|
498
|
-
|
|
499
|
-
const result = checkBrowserRelayAuth(req, url, config);
|
|
500
|
-
|
|
501
|
-
expect(result.ok).toBe(true);
|
|
502
|
-
if (!result.ok) throw new Error("expected ok");
|
|
503
|
-
expect(result.context.clientInstanceId).toBe("install-QUERY");
|
|
504
|
-
expect(result.context.guardianId).toBe(TEST_ACTOR_PRINCIPAL);
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
test("lifts clientInstanceId from the x-client-instance-id header", () => {
|
|
508
|
-
const token = mintEdgeToken(TEST_ACTOR_PRINCIPAL);
|
|
509
|
-
const config = makeConfig({});
|
|
510
|
-
const req = new Request(
|
|
511
|
-
`http://localhost:7830/v1/browser-relay?token=${token}`,
|
|
512
|
-
{
|
|
513
|
-
headers: {
|
|
514
|
-
upgrade: "websocket",
|
|
515
|
-
"x-client-instance-id": "install-HEADER",
|
|
516
|
-
},
|
|
517
|
-
},
|
|
518
|
-
);
|
|
519
|
-
const url = new URL(req.url);
|
|
520
|
-
|
|
521
|
-
const result = checkBrowserRelayAuth(req, url, config);
|
|
522
|
-
|
|
523
|
-
expect(result.ok).toBe(true);
|
|
524
|
-
if (!result.ok) throw new Error("expected ok");
|
|
525
|
-
expect(result.context.clientInstanceId).toBe("install-HEADER");
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
test("prefers x-client-instance-id header over query param when both are present", () => {
|
|
529
|
-
const token = mintEdgeToken(TEST_ACTOR_PRINCIPAL);
|
|
530
|
-
const config = makeConfig({});
|
|
531
|
-
const req = new Request(
|
|
532
|
-
`http://localhost:7830/v1/browser-relay?token=${token}&clientInstanceId=install-QUERY`,
|
|
533
|
-
{
|
|
534
|
-
headers: {
|
|
535
|
-
upgrade: "websocket",
|
|
536
|
-
"x-client-instance-id": "install-HEADER",
|
|
537
|
-
},
|
|
538
|
-
},
|
|
539
|
-
);
|
|
540
|
-
const url = new URL(req.url);
|
|
541
|
-
|
|
542
|
-
const result = checkBrowserRelayAuth(req, url, config);
|
|
543
|
-
|
|
544
|
-
expect(result.ok).toBe(true);
|
|
545
|
-
if (!result.ok) throw new Error("expected ok");
|
|
546
|
-
// The header form is considered the more explicit signal when
|
|
547
|
-
// both are set, matching the runtime's own precedence.
|
|
548
|
-
expect(result.context.clientInstanceId).toBe("install-HEADER");
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
test("treats an empty clientInstanceId query param as absent", () => {
|
|
552
|
-
const token = mintEdgeToken(TEST_ACTOR_PRINCIPAL);
|
|
553
|
-
const config = makeConfig({});
|
|
554
|
-
const req = new Request(
|
|
555
|
-
`http://localhost:7830/v1/browser-relay?token=${token}&clientInstanceId=`,
|
|
556
|
-
{ headers: { upgrade: "websocket" } },
|
|
557
|
-
);
|
|
558
|
-
const url = new URL(req.url);
|
|
559
|
-
|
|
560
|
-
const result = checkBrowserRelayAuth(req, url, config);
|
|
561
|
-
|
|
562
|
-
expect(result.ok).toBe(true);
|
|
563
|
-
if (!result.ok) throw new Error("expected ok");
|
|
564
|
-
expect(result.context.clientInstanceId).toBeUndefined();
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
test("returns undefined clientInstanceId when neither form is present", () => {
|
|
568
|
-
const token = mintEdgeToken(TEST_ACTOR_PRINCIPAL);
|
|
569
|
-
const config = makeConfig({});
|
|
570
|
-
const req = new Request(
|
|
571
|
-
`http://localhost:7830/v1/browser-relay?token=${token}`,
|
|
572
|
-
{ headers: { upgrade: "websocket" } },
|
|
573
|
-
);
|
|
574
|
-
const url = new URL(req.url);
|
|
575
|
-
|
|
576
|
-
const result = checkBrowserRelayAuth(req, url, config);
|
|
577
|
-
|
|
578
|
-
expect(result.ok).toBe(true);
|
|
579
|
-
if (!result.ok) throw new Error("expected ok");
|
|
580
|
-
expect(result.context.clientInstanceId).toBeUndefined();
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
test("lifts clientInstanceId even on the auth-bypass path", () => {
|
|
584
|
-
// When runtime proxy auth is disabled, the gateway still needs to
|
|
585
|
-
// propagate the per-install identifier so multi-instance routing
|
|
586
|
-
// continues to work in dev-bypass environments.
|
|
587
|
-
const config = makeConfig({ runtimeProxyRequireAuth: false });
|
|
588
|
-
const req = new Request(
|
|
589
|
-
"http://localhost:7830/v1/browser-relay?clientInstanceId=install-BYPASS",
|
|
590
|
-
{ headers: { upgrade: "websocket" } },
|
|
591
|
-
);
|
|
592
|
-
const url = new URL(req.url);
|
|
593
|
-
|
|
594
|
-
const result = checkBrowserRelayAuth(req, url, config);
|
|
595
|
-
|
|
596
|
-
expect(result.ok).toBe(true);
|
|
597
|
-
if (!result.ok) throw new Error("expected ok");
|
|
598
|
-
expect(result.context.authBypassed).toBe(true);
|
|
599
|
-
expect(result.context.clientInstanceId).toBe("install-BYPASS");
|
|
600
|
-
});
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
describe("createBrowserRelayWebsocketHandler guardian propagation", () => {
|
|
604
|
-
test("upgrades with guardianId populated in auth context for actor edge tokens", () => {
|
|
605
|
-
const token = mintEdgeToken(TEST_ACTOR_PRINCIPAL);
|
|
606
|
-
const config = makeConfig({});
|
|
607
|
-
const handler = createBrowserRelayWebsocketHandler(config);
|
|
608
|
-
const req = new Request(
|
|
609
|
-
`http://localhost:7830/v1/browser-relay?token=${token}`,
|
|
610
|
-
{ headers: { upgrade: "websocket" } },
|
|
611
|
-
);
|
|
612
|
-
|
|
613
|
-
let capturedData: unknown = null;
|
|
614
|
-
const fakeServer = {
|
|
615
|
-
requestIP: mock(() => ({
|
|
616
|
-
address: "127.0.0.1",
|
|
617
|
-
family: "IPv4",
|
|
618
|
-
port: 54000,
|
|
619
|
-
})),
|
|
620
|
-
upgrade: mock((_req: Request, opts?: { data?: unknown }) => {
|
|
621
|
-
capturedData = opts?.data;
|
|
622
|
-
return true;
|
|
623
|
-
}),
|
|
624
|
-
} as unknown as import("bun").Server<any>;
|
|
625
|
-
|
|
626
|
-
const res = handler(req, fakeServer);
|
|
627
|
-
|
|
628
|
-
expect(res).toBeUndefined();
|
|
629
|
-
expect(fakeServer.upgrade).toHaveBeenCalledTimes(1);
|
|
630
|
-
expect(capturedData).toMatchObject({
|
|
631
|
-
wsType: "browser-relay",
|
|
632
|
-
auth: {
|
|
633
|
-
authenticated: true,
|
|
634
|
-
authBypassed: false,
|
|
635
|
-
guardianId: TEST_ACTOR_PRINCIPAL,
|
|
636
|
-
},
|
|
637
|
-
});
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
test("rejects service-style edge tokens before upgrade", () => {
|
|
641
|
-
const token = mintServiceEdgeToken();
|
|
642
|
-
const config = makeConfig({});
|
|
643
|
-
const handler = createBrowserRelayWebsocketHandler(config);
|
|
644
|
-
const req = new Request(
|
|
645
|
-
`http://localhost:7830/v1/browser-relay?token=${token}`,
|
|
646
|
-
{ headers: { upgrade: "websocket" } },
|
|
647
|
-
);
|
|
648
|
-
|
|
649
|
-
const fakeServer = {
|
|
650
|
-
requestIP: mock(() => ({
|
|
651
|
-
address: "127.0.0.1",
|
|
652
|
-
family: "IPv4",
|
|
653
|
-
port: 54000,
|
|
654
|
-
})),
|
|
655
|
-
upgrade: mock(() => true),
|
|
656
|
-
} as unknown as import("bun").Server<any>;
|
|
657
|
-
|
|
658
|
-
const res = handler(req, fakeServer);
|
|
659
|
-
|
|
660
|
-
expect(res).toBeInstanceOf(Response);
|
|
661
|
-
expect(res!.status).toBe(401);
|
|
662
|
-
expect(fakeServer.upgrade).not.toHaveBeenCalled();
|
|
663
|
-
});
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
describe("isLoopbackPeer", () => {
|
|
667
|
-
test("uses x-forwarded-for first hop when trustProxy is enabled", () => {
|
|
668
|
-
const req = new Request("http://localhost:7830/v1/browser-relay", {
|
|
669
|
-
headers: { "x-forwarded-for": "203.0.113.5, 127.0.0.1" },
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
const fakeServer = {
|
|
673
|
-
requestIP: mock(() => ({
|
|
674
|
-
address: "127.0.0.1",
|
|
675
|
-
family: "IPv4",
|
|
676
|
-
port: 54000,
|
|
677
|
-
})),
|
|
678
|
-
} as unknown as import("bun").Server<any>;
|
|
679
|
-
|
|
680
|
-
expect(isLoopbackPeer(fakeServer, req, { trustProxy: true })).toBe(false);
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
test("falls back to peer IP when trustProxy is disabled", () => {
|
|
684
|
-
const req = new Request("http://localhost:7830/v1/browser-relay", {
|
|
685
|
-
headers: { "x-forwarded-for": "203.0.113.5, 127.0.0.1" },
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
const fakeServer = {
|
|
689
|
-
requestIP: mock(() => ({
|
|
690
|
-
address: "127.0.0.1",
|
|
691
|
-
family: "IPv4",
|
|
692
|
-
port: 54000,
|
|
693
|
-
})),
|
|
694
|
-
} as unknown as import("bun").Server<any>;
|
|
695
|
-
|
|
696
|
-
expect(isLoopbackPeer(fakeServer, req, { trustProxy: false })).toBe(true);
|
|
697
|
-
});
|
|
698
|
-
});
|