@vellumai/vellum-gateway 0.7.0 → 0.7.1
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/ARCHITECTURE.md +5 -5
- package/Dockerfile +1 -0
- package/README.md +4 -8
- package/bun.lock +7 -0
- package/knip.json +2 -1
- package/package.json +2 -1
- package/src/__tests__/browser-relay-websocket.test.ts +0 -1
- package/src/__tests__/channel-verification-session-proxy.test.ts +0 -1
- 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 +11 -1
- 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 +5 -5
- package/src/__tests__/guardian-init-lockfile.test.ts +6 -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__/resolve-assistant.test.ts +0 -1
- 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-thread-tracking.test.ts +630 -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 +0 -1
- package/src/__tests__/whatsapp-download.test.ts +0 -1
- package/src/__tests__/whatsapp-webhook.test.ts +0 -1
- package/src/auth/ipc-route-policy.ts +217 -0
- package/src/cli/enable-proxy.ts +0 -1
- package/src/config.ts +0 -7
- package/src/db/connection.ts +65 -3
- package/src/db/schema.ts +62 -0
- package/src/feature-flag-registry.json +30 -30
- package/src/handlers/handle-inbound.ts +33 -0
- package/src/http/middleware/auth.ts +43 -0
- 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 +2 -2
- 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/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/runtime-health-proxy.ts +2 -2
- package/src/http/routes/slack-control-plane-proxy.ts +3 -20
- 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-voice-verify-callback.ts +282 -0
- package/src/http/routes/twilio-voice-webhook.test.ts +5 -1
- package/src/http/routes/twilio-voice-webhook.ts +37 -1
- package/src/http/routes/whatsapp-webhook.test.ts +0 -1
- package/src/index.ts +31 -33
- package/src/ipc/assistant-client.ts +8 -4
- package/src/post-assistant-ready.ts +5 -3
- package/src/risk/bash-risk-classifier.test.ts +0 -24
- package/src/risk/command-registry/commands/assistant.ts +3 -19
- package/src/risk/command-registry.test.ts +0 -15
- package/src/risk/risk-classifier-parity.test.ts +0 -2
- package/src/schema.ts +57 -37
- package/src/slack/normalize.test.ts +74 -0
- package/src/slack/normalize.ts +99 -32
- package/src/slack/socket-mode.ts +184 -22
- package/src/telegram/send.test.ts +0 -1
- package/src/verification/binding-helpers.ts +107 -0
- package/src/verification/code-parsing.ts +44 -0
- package/src/verification/contact-helpers.ts +205 -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/voice/verification.ts +456 -0
- package/src/webhook-pipeline.ts +4 -0
- package/src/__tests__/telegram-only-default.test.ts +0 -133
package/ARCHITECTURE.md
CHANGED
|
@@ -128,7 +128,7 @@ The assistant daemon does not read or distribute a feature-flag token. All featu
|
|
|
128
128
|
|
|
129
129
|
### Channel Verification Session Control-Plane Proxy
|
|
130
130
|
|
|
131
|
-
Channel verification session endpoints are exposed directly by the gateway and forwarded to runtime integration handlers
|
|
131
|
+
Channel verification session endpoints are exposed directly by the gateway and forwarded to runtime integration handlers for dedicated auth handling. This keeps assistant skills and user-facing tooling on gateway URLs only.
|
|
132
132
|
|
|
133
133
|
**Forwarded endpoints:**
|
|
134
134
|
|
|
@@ -158,7 +158,7 @@ The `/v1/guardian/refresh` endpoint is the only public ingress for rotating JWT
|
|
|
158
158
|
|
|
159
159
|
### Runtime Health Proxy
|
|
160
160
|
|
|
161
|
-
Runtime health is exposed directly by the gateway at `GET /v1/health` and forwarded to the runtime's `GET /v1/health` endpoint
|
|
161
|
+
Runtime health is exposed directly by the gateway at `GET /v1/health` and forwarded to the runtime's `GET /v1/health` endpoint for dedicated auth handling.
|
|
162
162
|
|
|
163
163
|
**Authentication boundary:**
|
|
164
164
|
|
|
@@ -175,7 +175,7 @@ Runtime health is exposed directly by the gateway at `GET /v1/health` and forwar
|
|
|
175
175
|
|
|
176
176
|
### Telegram + Contacts Control-Plane Proxies
|
|
177
177
|
|
|
178
|
-
Telegram integration setup/config endpoints and contacts/invites endpoints are also exposed directly by the gateway and forwarded to runtime handlers
|
|
178
|
+
Telegram integration setup/config endpoints and contacts/invites endpoints are also exposed directly by the gateway and forwarded to runtime handlers for dedicated auth handling.
|
|
179
179
|
|
|
180
180
|
**Forwarded Telegram endpoints:**
|
|
181
181
|
|
|
@@ -213,7 +213,7 @@ Telegram integration setup/config endpoints and contacts/invites endpoints are a
|
|
|
213
213
|
|
|
214
214
|
### Twilio Control-Plane Proxy
|
|
215
215
|
|
|
216
|
-
Twilio integration setup/config endpoints are exposed directly by the gateway and forwarded to runtime handlers
|
|
216
|
+
Twilio integration setup/config endpoints are exposed directly by the gateway and forwarded to runtime handlers for dedicated auth handling. This keeps skills and clients on gateway URLs exclusively.
|
|
217
217
|
|
|
218
218
|
**Forwarded endpoints:**
|
|
219
219
|
|
|
@@ -242,7 +242,7 @@ Twilio integration setup/config endpoints are exposed directly by the gateway an
|
|
|
242
242
|
|
|
243
243
|
### Channel Readiness Proxy
|
|
244
244
|
|
|
245
|
-
Channel readiness endpoints are exposed directly by the gateway and forwarded to runtime handlers
|
|
245
|
+
Channel readiness endpoints are exposed directly by the gateway and forwarded to runtime handlers for dedicated auth handling.
|
|
246
246
|
|
|
247
247
|
**Forwarded endpoints:**
|
|
248
248
|
|
package/Dockerfile
CHANGED
|
@@ -12,6 +12,7 @@ COPY --from=bun /usr/local/bin/bun /usr/local/bin/bun
|
|
|
12
12
|
COPY packages/assistant-client ./packages/assistant-client
|
|
13
13
|
COPY packages/ces-client ./packages/ces-client
|
|
14
14
|
COPY packages/service-contracts ./packages/service-contracts
|
|
15
|
+
COPY packages/slack-text ./packages/slack-text
|
|
15
16
|
|
|
16
17
|
# Install deps for shared packages that have their own file: dependencies.
|
|
17
18
|
RUN cd /app/packages/ces-client && bun install --frozen-lockfile
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vellum Gateway
|
|
2
2
|
|
|
3
|
-
Standalone service that serves as the public ingress boundary for all external webhooks and callbacks. It owns Telegram integration end-to-end, routes Twilio voice webhooks, handles OAuth callbacks, and
|
|
3
|
+
Standalone service that serves as the public ingress boundary for all external webhooks and callbacks. It owns Telegram integration end-to-end, routes Twilio voice webhooks, handles OAuth callbacks, and acts as an authenticated reverse proxy for the assistant runtime.
|
|
4
4
|
|
|
5
5
|
## Architecture
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ Telegram → gateway/ → Assistant Runtime (/v1/assistants/:id/channels/inbound
|
|
|
10
10
|
Client → gateway/ (Bearer auth) → Assistant Runtime (any path)
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
The web app is **not** in the Telegram request path.
|
|
13
|
+
The web app is **not** in the Telegram request path. All non-Telegram requests that don't match a dedicated gateway route are forwarded to the assistant runtime with bearer token authentication.
|
|
14
14
|
|
|
15
15
|
For ingress and channel architecture details, see [`ARCHITECTURE.md`](ARCHITECTURE.md).
|
|
16
16
|
|
|
@@ -218,13 +218,9 @@ The gateway is the **sole public ingress point** for all external webhooks. The
|
|
|
218
218
|
|
|
219
219
|
When the ingress public base URL is configured (via `ingress.publicBaseUrl` in workspace config, read through `ConfigFileCache`), the gateway prioritizes it as the canonical URL for Twilio signature validation. If the signature only validates against the raw local request URL (fallback), a warning is logged indicating potential drift between the configured ingress URL and the actual webhook registration. The raw URL fallback is preserved for local-dev operability.
|
|
220
220
|
|
|
221
|
-
##
|
|
221
|
+
## Runtime Proxy
|
|
222
222
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
## Runtime Proxy Mode
|
|
226
|
-
|
|
227
|
-
When the runtime proxy is enabled (via workspace config), the gateway forwards all non-Telegram HTTP requests to the assistant runtime. This allows the gateway to serve as a single ingress point for both Telegram and API traffic.
|
|
223
|
+
The gateway acts as the single ingress point for all traffic. Dedicated gateway routes (webhooks, control-plane proxies, health checks) are matched first; any request that doesn't match a specific route is forwarded to the assistant runtime via a catch-all proxy.
|
|
228
224
|
|
|
229
225
|
### Auth behavior
|
|
230
226
|
|
package/bun.lock
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"@vellumai/assistant-client": "file:../packages/assistant-client",
|
|
9
9
|
"@vellumai/ces-client": "file:../packages/ces-client",
|
|
10
10
|
"@vellumai/service-contracts": "file:../packages/service-contracts",
|
|
11
|
+
"@vellumai/slack-text": "file:../packages/slack-text",
|
|
11
12
|
"drizzle-kit": "0.30.6",
|
|
12
13
|
"drizzle-orm": "0.45.2",
|
|
13
14
|
"file-type": "21.3.0",
|
|
@@ -209,6 +210,8 @@
|
|
|
209
210
|
|
|
210
211
|
"@vellumai/service-contracts": ["@vellumai/service-contracts@file:../packages/service-contracts", { "dependencies": { "zod": "4.3.6" }, "devDependencies": { "@types/bun": "1.2.4", "typescript": "5.7.3" } }],
|
|
211
212
|
|
|
213
|
+
"@vellumai/slack-text": ["@vellumai/slack-text@file:../packages/slack-text", { "devDependencies": { "@types/bun": "1.3.10", "typescript": "5.9.3" } }],
|
|
214
|
+
|
|
212
215
|
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
|
213
216
|
|
|
214
217
|
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
|
@@ -499,6 +502,8 @@
|
|
|
499
502
|
|
|
500
503
|
"@vellumai/service-contracts/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
|
|
501
504
|
|
|
505
|
+
"@vellumai/slack-text/@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
|
506
|
+
|
|
502
507
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
|
503
508
|
|
|
504
509
|
"gel/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
|
@@ -559,6 +564,8 @@
|
|
|
559
564
|
|
|
560
565
|
"@vellumai/service-contracts/@types/bun/bun-types": ["bun-types@1.2.4", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-nDPymR207ZZEoWD4AavvEaa/KZe/qlrbMSchqpQwovPZCKc7pwMoENjEtHgMKaAjJhy+x6vfqSBA1QU3bJgs0Q=="],
|
|
561
566
|
|
|
567
|
+
"@vellumai/slack-text/@types/bun/bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
|
568
|
+
|
|
562
569
|
"gel/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
|
|
563
570
|
|
|
564
571
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
package/knip.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vellumai/vellum-gateway",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"@vellumai/assistant-client": "file:../packages/assistant-client",
|
|
28
28
|
"@vellumai/ces-client": "file:../packages/ces-client",
|
|
29
29
|
"@vellumai/service-contracts": "file:../packages/service-contracts",
|
|
30
|
+
"@vellumai/slack-text": "file:../packages/slack-text",
|
|
30
31
|
"drizzle-kit": "0.30.6",
|
|
31
32
|
"drizzle-orm": "0.45.2",
|
|
32
33
|
"file-type": "21.3.0",
|
|
@@ -51,7 +51,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
51
51
|
defaultAssistantId: undefined,
|
|
52
52
|
unmappedPolicy: "reject",
|
|
53
53
|
port: 7830,
|
|
54
|
-
runtimeProxyEnabled: false,
|
|
55
54
|
runtimeProxyRequireAuth: true,
|
|
56
55
|
shutdownDrainMs: 5000,
|
|
57
56
|
runtimeTimeoutMs: 30000,
|
|
@@ -27,7 +27,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
27
27
|
defaultAssistantId: undefined,
|
|
28
28
|
unmappedPolicy: "reject",
|
|
29
29
|
port: 7830,
|
|
30
|
-
runtimeProxyEnabled: false,
|
|
31
30
|
runtimeProxyRequireAuth: true,
|
|
32
31
|
shutdownDrainMs: 5000,
|
|
33
32
|
runtimeTimeoutMs: 30000,
|
|
@@ -17,7 +17,6 @@ describe("config: hardcoded defaults", () => {
|
|
|
17
17
|
default: 100 * 1024 * 1024,
|
|
18
18
|
});
|
|
19
19
|
expect(config.maxAttachmentConcurrency).toBe(3);
|
|
20
|
-
expect(config.runtimeProxyEnabled).toBe(false);
|
|
21
20
|
expect(config.runtimeProxyRequireAuth).toBe(true);
|
|
22
21
|
expect(config.trustProxy).toBe(false);
|
|
23
22
|
expect(config.unmappedPolicy).toBe("reject");
|
|
@@ -27,7 +27,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
27
27
|
defaultAssistantId: undefined,
|
|
28
28
|
unmappedPolicy: "reject",
|
|
29
29
|
port: 7830,
|
|
30
|
-
runtimeProxyEnabled: false,
|
|
31
30
|
runtimeProxyRequireAuth: true,
|
|
32
31
|
shutdownDrainMs: 5000,
|
|
33
32
|
runtimeTimeoutMs: 30000,
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { createServer } from "node:net";
|
|
2
|
+
import { createServer, type Server } from "node:net";
|
|
3
3
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
4
4
|
import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
|
|
9
|
+
import { startFakeAssistantIpc } from "./fake-assistant-ipc.js";
|
|
10
|
+
|
|
9
11
|
const TEST_SERVICE_TOKEN = "test-ces-service-token";
|
|
10
12
|
|
|
11
13
|
const testDir = join(tmpdir(), `gw-managed-${Date.now()}-${Math.random()}`);
|
|
@@ -54,6 +56,7 @@ let gatewayProc: ChildProcess | null = null;
|
|
|
54
56
|
let gatewayPort = 0;
|
|
55
57
|
let cesPort = 0;
|
|
56
58
|
let cesServer: ReturnType<typeof Bun.serve> | null = null;
|
|
59
|
+
let fakeAssistantIpc: Server | null = null;
|
|
57
60
|
|
|
58
61
|
/** Ask the OS for a free port by briefly binding to port 0. */
|
|
59
62
|
function getFreePort(): Promise<number> {
|
|
@@ -95,11 +98,14 @@ async function startGateway(): Promise<void> {
|
|
|
95
98
|
);
|
|
96
99
|
gatewayPort = await getFreePort();
|
|
97
100
|
|
|
101
|
+
const workspaceDir = join(testDir, ".vellum", "workspace");
|
|
102
|
+
fakeAssistantIpc = startFakeAssistantIpc(workspaceDir);
|
|
103
|
+
|
|
98
104
|
gatewayProc = spawn("bun", ["run", gatewayEntry], {
|
|
99
105
|
env: {
|
|
100
106
|
...process.env,
|
|
101
107
|
GATEWAY_SECURITY_DIR: join(testDir, ".vellum", "protected"),
|
|
102
|
-
VELLUM_WORKSPACE_DIR:
|
|
108
|
+
VELLUM_WORKSPACE_DIR: workspaceDir,
|
|
103
109
|
GATEWAY_PORT: String(gatewayPort),
|
|
104
110
|
CES_CREDENTIAL_URL: `http://127.0.0.1:${cesPort}`,
|
|
105
111
|
CES_SERVICE_TOKEN: TEST_SERVICE_TOKEN,
|
|
@@ -190,6 +196,8 @@ function startFakeCes(opts: {
|
|
|
190
196
|
}
|
|
191
197
|
|
|
192
198
|
afterEach(async () => {
|
|
199
|
+
fakeAssistantIpc?.close();
|
|
200
|
+
fakeAssistantIpc = null;
|
|
193
201
|
cesServer?.stop(true);
|
|
194
202
|
cesServer = null;
|
|
195
203
|
gatewayPort = 0;
|
|
@@ -17,8 +17,11 @@ import {
|
|
|
17
17
|
import { mkdirSync, renameSync, writeFileSync, rmSync } from "node:fs";
|
|
18
18
|
import { hostname, tmpdir, userInfo } from "node:os";
|
|
19
19
|
import { dirname, join } from "node:path";
|
|
20
|
+
import type { Server } from "node:net";
|
|
20
21
|
import { fileURLToPath } from "node:url";
|
|
21
22
|
|
|
23
|
+
import { startFakeAssistantIpc } from "./fake-assistant-ipc.js";
|
|
24
|
+
|
|
22
25
|
// ---------------------------------------------------------------------------
|
|
23
26
|
// Constants — must match credential-reader.ts
|
|
24
27
|
// ---------------------------------------------------------------------------
|
|
@@ -189,14 +192,19 @@ const gatewayEntry = join(gatewayRoot, "src", "index.ts");
|
|
|
189
192
|
|
|
190
193
|
let gatewayProc: ChildProcess | null = null;
|
|
191
194
|
let port = 0;
|
|
195
|
+
let fakeAssistantIpc: Server | null = null;
|
|
192
196
|
|
|
193
197
|
async function startGateway(): Promise<void> {
|
|
194
198
|
port = 49152 + Math.floor(Math.random() * 16383);
|
|
199
|
+
|
|
200
|
+
const workspaceDir = join(testDir, ".vellum", "workspace");
|
|
201
|
+
fakeAssistantIpc = startFakeAssistantIpc(workspaceDir);
|
|
202
|
+
|
|
195
203
|
gatewayProc = spawn("bun", ["run", gatewayEntry], {
|
|
196
204
|
env: {
|
|
197
205
|
...process.env,
|
|
198
206
|
GATEWAY_SECURITY_DIR: join(testDir, ".vellum", "protected"),
|
|
199
|
-
VELLUM_WORKSPACE_DIR:
|
|
207
|
+
VELLUM_WORKSPACE_DIR: workspaceDir,
|
|
200
208
|
GATEWAY_PORT: String(port),
|
|
201
209
|
// Ensure Telegram is NOT configured via env vars
|
|
202
210
|
TELEGRAM_BOT_TOKEN: "",
|
|
@@ -229,6 +237,8 @@ async function startGateway(): Promise<void> {
|
|
|
229
237
|
}
|
|
230
238
|
|
|
231
239
|
afterEach(async () => {
|
|
240
|
+
fakeAssistantIpc?.close();
|
|
241
|
+
fakeAssistantIpc = null;
|
|
232
242
|
if (gatewayProc) {
|
|
233
243
|
const proc = gatewayProc;
|
|
234
244
|
gatewayProc = null;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { afterEach, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
realpathSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
symlinkSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { homedir, tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
import { initGatewayDb, resetGatewayDb } from "../db/connection.js";
|
|
15
|
+
|
|
16
|
+
const originalSecurityDir = process.env.GATEWAY_SECURITY_DIR;
|
|
17
|
+
const originalAllowRealSecurity =
|
|
18
|
+
process.env.VELLUM_ALLOW_REAL_GATEWAY_SECURITY_IN_TESTS;
|
|
19
|
+
const originalTestRealSecurity =
|
|
20
|
+
process.env.VELLUM_TEST_REAL_GATEWAY_SECURITY_DIR;
|
|
21
|
+
const originalHome = process.env.HOME;
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
resetGatewayDb();
|
|
25
|
+
if (originalSecurityDir === undefined) {
|
|
26
|
+
delete process.env.GATEWAY_SECURITY_DIR;
|
|
27
|
+
} else {
|
|
28
|
+
process.env.GATEWAY_SECURITY_DIR = originalSecurityDir;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (originalAllowRealSecurity === undefined) {
|
|
32
|
+
delete process.env.VELLUM_ALLOW_REAL_GATEWAY_SECURITY_IN_TESTS;
|
|
33
|
+
} else {
|
|
34
|
+
process.env.VELLUM_ALLOW_REAL_GATEWAY_SECURITY_IN_TESTS =
|
|
35
|
+
originalAllowRealSecurity;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (originalTestRealSecurity === undefined) {
|
|
39
|
+
delete process.env.VELLUM_TEST_REAL_GATEWAY_SECURITY_DIR;
|
|
40
|
+
} else {
|
|
41
|
+
process.env.VELLUM_TEST_REAL_GATEWAY_SECURITY_DIR =
|
|
42
|
+
originalTestRealSecurity;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (originalHome === undefined) {
|
|
46
|
+
delete process.env.HOME;
|
|
47
|
+
} else {
|
|
48
|
+
process.env.HOME = originalHome;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("initGatewayDb refuses test runs without an isolated security dir", async () => {
|
|
53
|
+
resetGatewayDb();
|
|
54
|
+
delete process.env.GATEWAY_SECURITY_DIR;
|
|
55
|
+
delete process.env.VELLUM_ALLOW_REAL_GATEWAY_SECURITY_IN_TESTS;
|
|
56
|
+
|
|
57
|
+
await expect(initGatewayDb()).rejects.toThrow(
|
|
58
|
+
"Refusing to open the gateway DB during tests without GATEWAY_SECURITY_DIR",
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("initGatewayDb refuses the real security dir during tests even when explicitly set", async () => {
|
|
63
|
+
resetGatewayDb();
|
|
64
|
+
process.env.GATEWAY_SECURITY_DIR = join(homedir(), ".vellum", "protected");
|
|
65
|
+
delete process.env.VELLUM_ALLOW_REAL_GATEWAY_SECURITY_IN_TESTS;
|
|
66
|
+
|
|
67
|
+
await expect(initGatewayDb()).rejects.toThrow(
|
|
68
|
+
"Refusing to open the real gateway security DB during tests",
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("initGatewayDb refuses symlink aliases to the real security dir during tests", async () => {
|
|
73
|
+
resetGatewayDb();
|
|
74
|
+
const testRoot = realpathSync(
|
|
75
|
+
mkdtempSync(join(tmpdir(), "vellum-gateway-db-isolation-")),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const fakeHome = join(testRoot, "home");
|
|
80
|
+
const realSecurityDir = join(fakeHome, ".vellum", "protected");
|
|
81
|
+
const aliasParent = join(testRoot, "aliases");
|
|
82
|
+
const securityAlias = join(aliasParent, "gateway-security-link");
|
|
83
|
+
|
|
84
|
+
mkdirSync(realSecurityDir, { recursive: true });
|
|
85
|
+
mkdirSync(aliasParent, { recursive: true });
|
|
86
|
+
symlinkSync(realSecurityDir, securityAlias, "dir");
|
|
87
|
+
|
|
88
|
+
process.env.HOME = fakeHome;
|
|
89
|
+
process.env.GATEWAY_SECURITY_DIR = securityAlias;
|
|
90
|
+
process.env.VELLUM_TEST_REAL_GATEWAY_SECURITY_DIR = realSecurityDir;
|
|
91
|
+
delete process.env.VELLUM_ALLOW_REAL_GATEWAY_SECURITY_IN_TESTS;
|
|
92
|
+
|
|
93
|
+
await expect(initGatewayDb()).rejects.toThrow(
|
|
94
|
+
"Refusing to open the real gateway security DB during tests",
|
|
95
|
+
);
|
|
96
|
+
} finally {
|
|
97
|
+
rmSync(testRoot, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("initGatewayDb refuses missing children under symlink aliases to the real security dir", async () => {
|
|
102
|
+
resetGatewayDb();
|
|
103
|
+
const testRoot = realpathSync(
|
|
104
|
+
mkdtempSync(join(tmpdir(), "vellum-gateway-db-isolation-")),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const fakeHome = join(testRoot, "home");
|
|
109
|
+
const realSecurityDir = join(fakeHome, ".vellum", "protected");
|
|
110
|
+
const aliasParent = join(testRoot, "aliases");
|
|
111
|
+
const securityLink = join(aliasParent, "gateway-security-link");
|
|
112
|
+
const missingChild = join(securityLink, "new-security-dir");
|
|
113
|
+
|
|
114
|
+
mkdirSync(realSecurityDir, { recursive: true });
|
|
115
|
+
mkdirSync(aliasParent, { recursive: true });
|
|
116
|
+
symlinkSync(realSecurityDir, securityLink, "dir");
|
|
117
|
+
|
|
118
|
+
process.env.HOME = fakeHome;
|
|
119
|
+
process.env.GATEWAY_SECURITY_DIR = missingChild;
|
|
120
|
+
process.env.VELLUM_TEST_REAL_GATEWAY_SECURITY_DIR = realSecurityDir;
|
|
121
|
+
delete process.env.VELLUM_ALLOW_REAL_GATEWAY_SECURITY_IN_TESTS;
|
|
122
|
+
|
|
123
|
+
await expect(initGatewayDb()).rejects.toThrow(
|
|
124
|
+
"Refusing to open the real gateway security DB during tests",
|
|
125
|
+
);
|
|
126
|
+
} finally {
|
|
127
|
+
rmSync(testRoot, { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("initGatewayDb does not migrate legacy gateway DBs during tests", async () => {
|
|
132
|
+
resetGatewayDb();
|
|
133
|
+
const testRoot = realpathSync(
|
|
134
|
+
mkdtempSync(join(tmpdir(), "vellum-gateway-db-isolation-")),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const fakeHome = join(testRoot, "home");
|
|
139
|
+
const legacyDir = join(fakeHome, ".vellum", "data");
|
|
140
|
+
const legacyDb = join(legacyDir, "gateway.sqlite");
|
|
141
|
+
const securityDir = join(testRoot, "gateway-security");
|
|
142
|
+
|
|
143
|
+
mkdirSync(legacyDir, { recursive: true });
|
|
144
|
+
writeFileSync(legacyDb, "legacy gateway db");
|
|
145
|
+
|
|
146
|
+
process.env.HOME = fakeHome;
|
|
147
|
+
process.env.GATEWAY_SECURITY_DIR = securityDir;
|
|
148
|
+
delete process.env.VELLUM_ALLOW_REAL_GATEWAY_SECURITY_IN_TESTS;
|
|
149
|
+
|
|
150
|
+
await initGatewayDb();
|
|
151
|
+
|
|
152
|
+
expect(existsSync(legacyDb)).toBe(true);
|
|
153
|
+
expect(existsSync(join(securityDir, "gateway.sqlite"))).toBe(true);
|
|
154
|
+
} finally {
|
|
155
|
+
rmSync(testRoot, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal fake assistant IPC server for tests.
|
|
3
|
+
*
|
|
4
|
+
* Listens on assistant.sock inside the given workspace dir and responds
|
|
5
|
+
* to the "health" JSON-RPC call with { status: "ok" }. This satisfies
|
|
6
|
+
* the gateway's waitForAssistant() poll so it starts immediately.
|
|
7
|
+
*/
|
|
8
|
+
import { createServer, type Server } from "node:net";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { mkdirSync } from "node:fs";
|
|
11
|
+
|
|
12
|
+
export function startFakeAssistantIpc(workspaceDir: string): Server {
|
|
13
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
14
|
+
const socketPath = join(workspaceDir, "assistant.sock");
|
|
15
|
+
|
|
16
|
+
const server = createServer((conn) => {
|
|
17
|
+
let buffer = "";
|
|
18
|
+
conn.on("data", (chunk) => {
|
|
19
|
+
buffer += chunk.toString();
|
|
20
|
+
let idx: number;
|
|
21
|
+
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
22
|
+
const line = buffer.slice(0, idx).trim();
|
|
23
|
+
buffer = buffer.slice(idx + 1);
|
|
24
|
+
if (!line) continue;
|
|
25
|
+
try {
|
|
26
|
+
const req = JSON.parse(line) as { id: string; method: string };
|
|
27
|
+
conn.write(
|
|
28
|
+
JSON.stringify({ id: req.id, result: { status: "ok" } }) + "\n",
|
|
29
|
+
);
|
|
30
|
+
} catch {
|
|
31
|
+
// ignore malformed
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
server.listen(socketPath);
|
|
38
|
+
return server;
|
|
39
|
+
}
|
|
@@ -41,7 +41,7 @@ const TEST_REGISTRY = {
|
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
id: "user-hosted-enabled",
|
|
44
|
-
scope: "
|
|
44
|
+
scope: "client",
|
|
45
45
|
key: "user-hosted-enabled",
|
|
46
46
|
label: "User Hosted Enabled",
|
|
47
47
|
description: "Enable user-hosted onboarding flow",
|
|
@@ -103,7 +103,7 @@ describe("GET /v1/feature-flags handler", () => {
|
|
|
103
103
|
const defaults = loadFeatureFlagDefaults();
|
|
104
104
|
const declaredKeys = Object.keys(defaults);
|
|
105
105
|
|
|
106
|
-
// Should return all declared assistant-scope flags (not
|
|
106
|
+
// Should return all declared assistant-scope flags (not client-scope)
|
|
107
107
|
expect(body.flags.length).toBe(declaredKeys.length);
|
|
108
108
|
expect(body.flags.length).toBeGreaterThan(0);
|
|
109
109
|
|
|
@@ -159,11 +159,11 @@ describe("GET /v1/feature-flags handler", () => {
|
|
|
159
159
|
expect(res.status).toBe(200);
|
|
160
160
|
const body = await res.json();
|
|
161
161
|
|
|
162
|
-
// The
|
|
163
|
-
const
|
|
162
|
+
// The client-scope flag should not appear
|
|
163
|
+
const clientFlag = body.flags.find(
|
|
164
164
|
(f: { key: string }) => f.key === "user-hosted-enabled",
|
|
165
165
|
);
|
|
166
|
-
expect(
|
|
166
|
+
expect(clientFlag).toBeUndefined();
|
|
167
167
|
});
|
|
168
168
|
|
|
169
169
|
test("returns all declared flags even when store has no persisted values", async () => {
|
|
@@ -138,7 +138,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
138
138
|
defaultAssistantId: undefined,
|
|
139
139
|
unmappedPolicy: "reject",
|
|
140
140
|
port: 7830,
|
|
141
|
-
runtimeProxyEnabled: false,
|
|
142
141
|
runtimeProxyRequireAuth: true,
|
|
143
142
|
shutdownDrainMs: 5000,
|
|
144
143
|
runtimeTimeoutMs: 30000,
|
|
@@ -359,9 +358,12 @@ describe("guardian/init one-time-use lockfile", () => {
|
|
|
359
358
|
const body = await res.json();
|
|
360
359
|
|
|
361
360
|
// Verify contact records were written to the assistant DB
|
|
362
|
-
const assistantDb = new Database(
|
|
363
|
-
|
|
364
|
-
|
|
361
|
+
const assistantDb = new Database(
|
|
362
|
+
join(testRoot, "data", "db", "assistant.db"),
|
|
363
|
+
{
|
|
364
|
+
readonly: true,
|
|
365
|
+
},
|
|
366
|
+
);
|
|
365
367
|
|
|
366
368
|
const contact = assistantDb
|
|
367
369
|
.query<
|
|
@@ -45,7 +45,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
45
45
|
defaultAssistantId: undefined,
|
|
46
46
|
unmappedPolicy: "reject",
|
|
47
47
|
port: 7830,
|
|
48
|
-
runtimeProxyEnabled: false,
|
|
49
48
|
runtimeProxyRequireAuth: true,
|
|
50
49
|
shutdownDrainMs: 5000,
|
|
51
50
|
runtimeTimeoutMs: 30000,
|
|
@@ -22,7 +22,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
22
22
|
defaultAssistantId: undefined,
|
|
23
23
|
unmappedPolicy: "reject",
|
|
24
24
|
port: 7830,
|
|
25
|
-
runtimeProxyEnabled: false,
|
|
26
25
|
runtimeProxyRequireAuth: true,
|
|
27
26
|
shutdownDrainMs: 5000,
|
|
28
27
|
runtimeTimeoutMs: 30000,
|
|
@@ -30,7 +30,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
30
30
|
defaultAssistantId: undefined,
|
|
31
31
|
unmappedPolicy: "reject",
|
|
32
32
|
port: 7830,
|
|
33
|
-
runtimeProxyEnabled: false,
|
|
34
33
|
runtimeProxyRequireAuth: true,
|
|
35
34
|
shutdownDrainMs: 5000,
|
|
36
35
|
runtimeTimeoutMs: 30000,
|
|
@@ -34,7 +34,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
34
34
|
defaultAssistantId: undefined,
|
|
35
35
|
unmappedPolicy: "reject",
|
|
36
36
|
port: 7830,
|
|
37
|
-
runtimeProxyEnabled: false,
|
|
38
37
|
runtimeProxyRequireAuth: true,
|
|
39
38
|
shutdownDrainMs: 5000,
|
|
40
39
|
runtimeTimeoutMs: 30000,
|
|
@@ -9,7 +9,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
9
9
|
defaultAssistantId: undefined,
|
|
10
10
|
unmappedPolicy: "reject",
|
|
11
11
|
port: 7830,
|
|
12
|
-
runtimeProxyEnabled: false,
|
|
13
12
|
runtimeProxyRequireAuth: true,
|
|
14
13
|
shutdownDrainMs: 5000,
|
|
15
14
|
runtimeTimeoutMs: 30000,
|
|
@@ -38,7 +38,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
38
38
|
defaultAssistantId: undefined,
|
|
39
39
|
unmappedPolicy: "reject",
|
|
40
40
|
port: 7830,
|
|
41
|
-
runtimeProxyEnabled: false,
|
|
42
41
|
runtimeProxyRequireAuth: true,
|
|
43
42
|
shutdownDrainMs: 5000,
|
|
44
43
|
runtimeTimeoutMs: 30000,
|
|
@@ -27,7 +27,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
27
27
|
defaultAssistantId: undefined,
|
|
28
28
|
unmappedPolicy: "reject",
|
|
29
29
|
port: 7830,
|
|
30
|
-
runtimeProxyEnabled: false,
|
|
31
30
|
runtimeProxyRequireAuth: true,
|
|
32
31
|
shutdownDrainMs: 5000,
|
|
33
32
|
runtimeTimeoutMs: 30000,
|
|
@@ -41,7 +41,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
41
41
|
defaultAssistantId: undefined,
|
|
42
42
|
unmappedPolicy: "reject",
|
|
43
43
|
port: 7830,
|
|
44
|
-
runtimeProxyEnabled: true,
|
|
45
44
|
runtimeProxyRequireAuth: true,
|
|
46
45
|
shutdownDrainMs: 5000,
|
|
47
46
|
runtimeTimeoutMs: 30000,
|
|
@@ -27,7 +27,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
27
27
|
defaultAssistantId: undefined,
|
|
28
28
|
unmappedPolicy: "reject",
|
|
29
29
|
port: 7830,
|
|
30
|
-
runtimeProxyEnabled: true,
|
|
31
30
|
runtimeProxyRequireAuth: false,
|
|
32
31
|
shutdownDrainMs: 5000,
|
|
33
32
|
runtimeTimeoutMs: 30000,
|
|
@@ -27,7 +27,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
27
27
|
defaultAssistantId: undefined,
|
|
28
28
|
unmappedPolicy: "reject",
|
|
29
29
|
port: 7830,
|
|
30
|
-
runtimeProxyEnabled: false,
|
|
31
30
|
runtimeProxyRequireAuth: true,
|
|
32
31
|
shutdownDrainMs: 5000,
|
|
33
32
|
runtimeTimeoutMs: 30000,
|