@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,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ATL-433: /v1/pair validates Origin against KNOWN_EXTENSION_ORIGINS,
|
|
3
|
+
* and resolveExtensionOrigin uses the same allowlist.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, test, expect, mock, beforeEach } from "bun:test";
|
|
6
|
+
|
|
7
|
+
import { initSigningKey } from "../auth/token-service.js";
|
|
8
|
+
|
|
9
|
+
// Must init signing key before any module that mints tokens is imported.
|
|
10
|
+
initSigningKey(Buffer.from("test-signing-key-at-least-32-bytes-long-xx"));
|
|
11
|
+
|
|
12
|
+
// Mock DB — pair.ts calls resolveLocalGuardianPrincipalId() which queries the DB.
|
|
13
|
+
const mockQuery = mock();
|
|
14
|
+
mock.module("../db/assistant-db-proxy.js", () => ({
|
|
15
|
+
assistantDbQuery: mockQuery,
|
|
16
|
+
assistantDbRun: mock(),
|
|
17
|
+
assistantDbExec: mock(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const { handlePair, resetPairRateLimiterForTests } = await import(
|
|
21
|
+
"../http/routes/pair.js"
|
|
22
|
+
);
|
|
23
|
+
const { resolveExtensionOrigin } = await import(
|
|
24
|
+
"../http/middleware/cors.js"
|
|
25
|
+
);
|
|
26
|
+
const { KNOWN_EXTENSION_ORIGINS } = await import(
|
|
27
|
+
"../chrome-extension-origins.js"
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Simulate a loopback peer IP as supplied by the gateway server to the handler.
|
|
31
|
+
const LOOPBACK_IP = "127.0.0.1";
|
|
32
|
+
|
|
33
|
+
// A valid Vellum extension origin (production).
|
|
34
|
+
const PROD_ORIGIN = "chrome-extension://hphbdmpffeigpcdjkckleobjmhhokpne";
|
|
35
|
+
// A non-Vellum extension origin.
|
|
36
|
+
const MALICIOUS_ORIGIN = "chrome-extension://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
37
|
+
|
|
38
|
+
function makePairRequest(overrides: {
|
|
39
|
+
method?: string;
|
|
40
|
+
origin?: string | null;
|
|
41
|
+
interfaceId?: string | null;
|
|
42
|
+
xForwardedFor?: string;
|
|
43
|
+
} = {}): Request {
|
|
44
|
+
const { method = "POST", origin, interfaceId = "chrome-extension", xForwardedFor } = overrides;
|
|
45
|
+
const headers: Record<string, string> = {
|
|
46
|
+
"host": "localhost:7830",
|
|
47
|
+
"content-type": "application/json",
|
|
48
|
+
};
|
|
49
|
+
if (origin !== null) {
|
|
50
|
+
headers["origin"] = origin ?? PROD_ORIGIN;
|
|
51
|
+
}
|
|
52
|
+
if (interfaceId !== null) {
|
|
53
|
+
headers["x-vellum-interface-id"] = interfaceId;
|
|
54
|
+
}
|
|
55
|
+
if (xForwardedFor) {
|
|
56
|
+
headers["x-forwarded-for"] = xForwardedFor;
|
|
57
|
+
}
|
|
58
|
+
return new Request("http://localhost:7830/v1/pair", { method, headers });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
resetPairRateLimiterForTests();
|
|
63
|
+
mockQuery.mockResolvedValue([{ principalId: "guardian-001" }]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// resolveExtensionOrigin — allowlist behaviour
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
describe("resolveExtensionOrigin", () => {
|
|
71
|
+
test("returns origin for every known Vellum extension ID", () => {
|
|
72
|
+
for (const origin of KNOWN_EXTENSION_ORIGINS) {
|
|
73
|
+
const req = new Request("http://localhost:7830/v1/events", {
|
|
74
|
+
headers: { origin },
|
|
75
|
+
});
|
|
76
|
+
expect(resolveExtensionOrigin(req)).toBe(origin);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("returns null for an unknown chrome-extension:// origin", () => {
|
|
81
|
+
const req = new Request("http://localhost:7830/v1/events", {
|
|
82
|
+
headers: { origin: MALICIOUS_ORIGIN },
|
|
83
|
+
});
|
|
84
|
+
expect(resolveExtensionOrigin(req)).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("returns null when Origin header is absent", () => {
|
|
88
|
+
const req = new Request("http://localhost:7830/v1/events");
|
|
89
|
+
expect(resolveExtensionOrigin(req)).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("returns null for a non-extension origin (web page)", () => {
|
|
93
|
+
const req = new Request("http://localhost:7830/v1/events", {
|
|
94
|
+
headers: { origin: "https://evil.example.com" },
|
|
95
|
+
});
|
|
96
|
+
expect(resolveExtensionOrigin(req)).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// /v1/pair — Origin check for chrome-extension interface
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
describe("handlePair — Origin allowlist", () => {
|
|
105
|
+
test("pairs successfully with a known prod extension origin", async () => {
|
|
106
|
+
const req = makePairRequest({ origin: PROD_ORIGIN });
|
|
107
|
+
const res = await handlePair(req, LOOPBACK_IP);
|
|
108
|
+
expect(res.status).toBe(200);
|
|
109
|
+
const body = await res.json() as Record<string, unknown>;
|
|
110
|
+
expect(typeof body.token).toBe("string");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("pairs successfully with each known extension origin", async () => {
|
|
114
|
+
for (const origin of KNOWN_EXTENSION_ORIGINS) {
|
|
115
|
+
resetPairRateLimiterForTests();
|
|
116
|
+
const req = makePairRequest({ origin });
|
|
117
|
+
const res = await handlePair(req, LOOPBACK_IP);
|
|
118
|
+
expect(res.status).toBe(200);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("rejects a request from an unknown extension origin with 403", async () => {
|
|
123
|
+
const req = makePairRequest({ origin: MALICIOUS_ORIGIN });
|
|
124
|
+
const res = await handlePair(req, LOOPBACK_IP);
|
|
125
|
+
expect(res.status).toBe(403);
|
|
126
|
+
const body = await res.json() as Record<string, unknown>;
|
|
127
|
+
expect((body.error as Record<string, unknown>).code).toBe("FORBIDDEN");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("rejects a request with no Origin header with 403", async () => {
|
|
131
|
+
const req = makePairRequest({ origin: null });
|
|
132
|
+
const res = await handlePair(req, LOOPBACK_IP);
|
|
133
|
+
expect(res.status).toBe(403);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("still rejects non-loopback callers regardless of origin", async () => {
|
|
137
|
+
const req = makePairRequest({ origin: PROD_ORIGIN });
|
|
138
|
+
const res = await handlePair(req, "8.8.8.8");
|
|
139
|
+
expect(res.status).toBe(403);
|
|
140
|
+
const body = await res.json() as Record<string, unknown>;
|
|
141
|
+
expect((body.error as Record<string, unknown>).code).toBe("FORBIDDEN");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("still rejects requests with X-Forwarded-For regardless of origin", async () => {
|
|
145
|
+
const req = makePairRequest({ origin: PROD_ORIGIN, xForwardedFor: "1.2.3.4" });
|
|
146
|
+
const res = await handlePair(req, LOOPBACK_IP);
|
|
147
|
+
expect(res.status).toBe(403);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("still rejects unknown interface IDs (unrelated to origin check)", async () => {
|
|
151
|
+
const req = makePairRequest({ origin: PROD_ORIGIN, interfaceId: "unknown-client" });
|
|
152
|
+
const res = await handlePair(req, LOOPBACK_IP);
|
|
153
|
+
expect(res.status).toBe(400);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -49,7 +49,7 @@ describe("checkAuthRateLimit loopback exemption", () => {
|
|
|
49
49
|
const ip = "203.0.113.5";
|
|
50
50
|
const limiter = blockedLimiter(ip);
|
|
51
51
|
const res = checkAuthRateLimit(
|
|
52
|
-
new URL("http://local/
|
|
52
|
+
new URL("http://local/health"),
|
|
53
53
|
limiter,
|
|
54
54
|
ip,
|
|
55
55
|
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect, mock, beforeEach, afterEach } from "bun:test";
|
|
2
|
-
import { mkdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
3
4
|
|
|
4
5
|
import type { CredentialCache } from "../credential-cache.js";
|
|
5
6
|
|
|
@@ -32,6 +33,37 @@ const { RemoteFeatureFlagSync } =
|
|
|
32
33
|
await import("../remote-feature-flag-sync.js");
|
|
33
34
|
const { readRemoteFeatureFlags, clearRemoteFeatureFlagStoreCache } =
|
|
34
35
|
await import("../feature-flag-remote-store.js");
|
|
36
|
+
const { resetFeatureFlagDefaultsCache, _setRegistryCandidateOverrides } =
|
|
37
|
+
await import("../feature-flag-defaults.js");
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Test-local registry with a GA flag (defaultEnabled: true) for the
|
|
41
|
+
// "ignores remote false for GA flags" test. Written to an isolated temp path
|
|
42
|
+
// so we never touch the committed registry file.
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
const testRegistryPath = join(protectedDir, "feature-flag-registry.json");
|
|
45
|
+
|
|
46
|
+
const TEST_REGISTRY = {
|
|
47
|
+
version: 1,
|
|
48
|
+
flags: [
|
|
49
|
+
{
|
|
50
|
+
id: "test-ga-flag",
|
|
51
|
+
scope: "assistant",
|
|
52
|
+
key: "test-ga-flag",
|
|
53
|
+
label: "Test GA Flag",
|
|
54
|
+
description: "A test flag that is GA (defaultEnabled: true)",
|
|
55
|
+
defaultEnabled: true,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "email-channel",
|
|
59
|
+
scope: "assistant",
|
|
60
|
+
key: "email-channel",
|
|
61
|
+
label: "Email Channel",
|
|
62
|
+
description: "Email channel integration",
|
|
63
|
+
defaultEnabled: false,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
35
67
|
|
|
36
68
|
// ---------------------------------------------------------------------------
|
|
37
69
|
// Helpers
|
|
@@ -92,6 +124,10 @@ beforeEach(() => {
|
|
|
92
124
|
delete process.env.VELLUM_PLATFORM_URL;
|
|
93
125
|
delete process.env.PLATFORM_INTERNAL_API_KEY;
|
|
94
126
|
mkdirSync(protectedDir, { recursive: true });
|
|
127
|
+
// Write the test registry and point resolution at it
|
|
128
|
+
writeFileSync(testRegistryPath, JSON.stringify(TEST_REGISTRY, null, 2));
|
|
129
|
+
_setRegistryCandidateOverrides([testRegistryPath]);
|
|
130
|
+
resetFeatureFlagDefaultsCache();
|
|
95
131
|
clearRemoteFeatureFlagStoreCache();
|
|
96
132
|
fetchMock = mock(async () => new Response());
|
|
97
133
|
});
|
|
@@ -113,6 +149,8 @@ afterEach(() => {
|
|
|
113
149
|
} catch {
|
|
114
150
|
// best effort cleanup
|
|
115
151
|
}
|
|
152
|
+
_setRegistryCandidateOverrides(null);
|
|
153
|
+
resetFeatureFlagDefaultsCache();
|
|
116
154
|
clearRemoteFeatureFlagStoreCache();
|
|
117
155
|
});
|
|
118
156
|
|
|
@@ -423,15 +461,17 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
423
461
|
// The platform sends false for all flags it knows about (blanket-deny).
|
|
424
462
|
// GA flags (defaultEnabled: true in the registry) should not be disabled
|
|
425
463
|
// by remote overrides — only local persisted overrides can do that.
|
|
464
|
+
// Uses the test-local registry which defines test-ga-flag as GA
|
|
465
|
+
// (defaultEnabled: true) and email-channel as gated (defaultEnabled: false).
|
|
426
466
|
fetchMock = mock(async () =>
|
|
427
467
|
Response.json({
|
|
428
468
|
flags: {
|
|
429
469
|
// GA flag (defaultEnabled: true) — remote false should be dropped
|
|
430
|
-
"
|
|
470
|
+
"test-ga-flag": false,
|
|
431
471
|
// Gated flag (defaultEnabled: false) — remote false is kept
|
|
432
472
|
"email-channel": false,
|
|
433
473
|
// GA flag set to true — should be kept (redundant but harmless)
|
|
434
|
-
|
|
474
|
+
"test-ga-flag-true": true,
|
|
435
475
|
// Unknown flag — remote false is kept (not in registry)
|
|
436
476
|
"unknown-flag": false,
|
|
437
477
|
},
|
|
@@ -446,12 +486,12 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
446
486
|
|
|
447
487
|
clearRemoteFeatureFlagStoreCache();
|
|
448
488
|
const cached = readRemoteFeatureFlags();
|
|
449
|
-
//
|
|
450
|
-
expect(cached["
|
|
489
|
+
// test-ga-flag (GA, remote false) should be absent
|
|
490
|
+
expect(cached["test-ga-flag"]).toBeUndefined();
|
|
451
491
|
// email-channel (gated, remote false) should be present
|
|
452
492
|
expect(cached["email-channel"]).toBe(false);
|
|
453
|
-
//
|
|
454
|
-
expect(cached
|
|
493
|
+
// test-ga-flag-true (unknown but true) should be present
|
|
494
|
+
expect(cached["test-ga-flag-true"]).toBe(true);
|
|
455
495
|
// unknown-flag (not in registry, remote false) should be present
|
|
456
496
|
expect(cached["unknown-flag"]).toBe(false);
|
|
457
497
|
});
|
|
@@ -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,
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
TWILIO_CONNECT_ACTION_WEBHOOK_PATH,
|
|
6
|
+
TWILIO_MEDIA_STREAM_WEBHOOK_PATH,
|
|
7
|
+
TWILIO_RELAY_WEBHOOK_PATH,
|
|
8
|
+
TWILIO_STATUS_WEBHOOK_PATH,
|
|
9
|
+
TWILIO_VOICE_WEBHOOK_PATH,
|
|
10
|
+
} from "@vellumai/service-contracts/twilio-ingress";
|
|
4
11
|
import { buildSchema } from "../schema.js";
|
|
5
12
|
|
|
6
13
|
/** A route extracted from source: path + optional HTTP method. */
|
|
@@ -9,6 +16,14 @@ interface ExtractedRoute {
|
|
|
9
16
|
method: string | null; // null means "any method"
|
|
10
17
|
}
|
|
11
18
|
|
|
19
|
+
const ROUTE_PATH_CONSTANTS: Record<string, string> = {
|
|
20
|
+
TWILIO_CONNECT_ACTION_WEBHOOK_PATH,
|
|
21
|
+
TWILIO_MEDIA_STREAM_WEBHOOK_PATH,
|
|
22
|
+
TWILIO_RELAY_WEBHOOK_PATH,
|
|
23
|
+
TWILIO_STATUS_WEBHOOK_PATH,
|
|
24
|
+
TWILIO_VOICE_WEBHOOK_PATH,
|
|
25
|
+
};
|
|
26
|
+
|
|
12
27
|
/**
|
|
13
28
|
* Extracts route paths from the gateway index.ts source code.
|
|
14
29
|
*
|
|
@@ -40,6 +55,17 @@ function extractRoutesFromSource(): ExtractedRoute[] {
|
|
|
40
55
|
continue;
|
|
41
56
|
}
|
|
42
57
|
|
|
58
|
+
// Match shared path constants: `path: SOME_WEBHOOK_PATH`
|
|
59
|
+
const constantMatch = line.match(/path:\s*([A-Z0-9_]+)\b/);
|
|
60
|
+
const constantPath = constantMatch
|
|
61
|
+
? ROUTE_PATH_CONSTANTS[constantMatch[1]]
|
|
62
|
+
: undefined;
|
|
63
|
+
if (constantPath) {
|
|
64
|
+
const method = findMethodNearPath(lines, i);
|
|
65
|
+
routes.push({ path: constantPath, method });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
43
69
|
// Match regex paths: `path: /^\/v1\/contacts\/([^/]+)$/`
|
|
44
70
|
const regexMatch = line.match(/path:\s*\/\^(.*?)\$\//);
|
|
45
71
|
if (regexMatch) {
|
|
@@ -57,6 +83,20 @@ function extractRoutesFromSource(): ExtractedRoute[] {
|
|
|
57
83
|
seenPreRouterPaths.add(preRouterMatch[1]);
|
|
58
84
|
routes.push({ path: preRouterMatch[1], method: null });
|
|
59
85
|
}
|
|
86
|
+
|
|
87
|
+
const preRouterConstantMatch = line.match(
|
|
88
|
+
/url\.pathname\s*===\s*([A-Z0-9_]+)\b/,
|
|
89
|
+
);
|
|
90
|
+
const preRouterConstantPath = preRouterConstantMatch
|
|
91
|
+
? ROUTE_PATH_CONSTANTS[preRouterConstantMatch[1]]
|
|
92
|
+
: undefined;
|
|
93
|
+
if (
|
|
94
|
+
preRouterConstantPath &&
|
|
95
|
+
!seenPreRouterPaths.has(preRouterConstantPath)
|
|
96
|
+
) {
|
|
97
|
+
seenPreRouterPaths.add(preRouterConstantPath);
|
|
98
|
+
routes.push({ path: preRouterConstantPath, method: null });
|
|
99
|
+
}
|
|
60
100
|
}
|
|
61
101
|
|
|
62
102
|
return routes;
|
|
@@ -125,14 +165,10 @@ function regexToOpenApiPath(escaped: string): string | null {
|
|
|
125
165
|
// ── Routes that are intentionally undocumented in the OpenAPI schema ──
|
|
126
166
|
// Each entry must have a comment explaining why it's excluded.
|
|
127
167
|
const EXCLUDED_FROM_SCHEMA = new Set([
|
|
128
|
-
// Browser relay WebSocket upgrade — handled pre-router, not a REST endpoint
|
|
129
|
-
"/v1/browser-relay",
|
|
130
|
-
|
|
131
|
-
// Browser extension pairing — localhost-only, no external consumers
|
|
132
|
-
"/v1/browser-extension-pair",
|
|
133
|
-
|
|
134
168
|
// Runtime proxy catch-all — documented as /{path} in the schema
|
|
135
169
|
"catch-all",
|
|
170
|
+
// Loopback-only pairing endpoint — not part of the public gateway API
|
|
171
|
+
"/v1/pair",
|
|
136
172
|
]);
|
|
137
173
|
|
|
138
174
|
// ── Schema paths that don't map to a discrete route definition ──
|
|
@@ -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,
|
|
@@ -39,7 +39,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
39
39
|
routingEntries: [],
|
|
40
40
|
runtimeInitialBackoffMs: 500,
|
|
41
41
|
runtimeMaxRetries: 2,
|
|
42
|
-
runtimeProxyEnabled: false,
|
|
43
42
|
runtimeProxyRequireAuth: false,
|
|
44
43
|
runtimeTimeoutMs: 30000,
|
|
45
44
|
shutdownDrainMs: 5000,
|
|
@@ -174,6 +173,7 @@ describe("normalizeSlackAppMention with display name", () => {
|
|
|
174
173
|
event,
|
|
175
174
|
"evt-dn-1a",
|
|
176
175
|
config,
|
|
176
|
+
undefined,
|
|
177
177
|
"xoxb-test",
|
|
178
178
|
);
|
|
179
179
|
expect(result1).not.toBeNull();
|
|
@@ -187,6 +187,7 @@ describe("normalizeSlackAppMention with display name", () => {
|
|
|
187
187
|
event,
|
|
188
188
|
"evt-dn-1b",
|
|
189
189
|
config,
|
|
190
|
+
undefined,
|
|
190
191
|
"xoxb-test",
|
|
191
192
|
);
|
|
192
193
|
expect(result2).not.toBeNull();
|
|
@@ -219,6 +220,7 @@ describe("normalizeSlackAppMention with display name", () => {
|
|
|
219
220
|
event,
|
|
220
221
|
"evt-dn-pw",
|
|
221
222
|
config,
|
|
223
|
+
undefined,
|
|
222
224
|
"xoxb-test",
|
|
223
225
|
);
|
|
224
226
|
expect(result).not.toBeNull();
|
|
@@ -226,6 +228,68 @@ describe("normalizeSlackAppMention with display name", () => {
|
|
|
226
228
|
expect(result!.event.actor.username).toBe("testuser");
|
|
227
229
|
});
|
|
228
230
|
|
|
231
|
+
test("renders cache-warmed mention labels in model-facing content", async () => {
|
|
232
|
+
fetchMock = mock(async () => {
|
|
233
|
+
return new Response(
|
|
234
|
+
JSON.stringify({
|
|
235
|
+
ok: true,
|
|
236
|
+
user: {
|
|
237
|
+
name: "leo",
|
|
238
|
+
real_name: "Leo Example",
|
|
239
|
+
profile: { display_name: "Leo" },
|
|
240
|
+
},
|
|
241
|
+
}),
|
|
242
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const config = makeConfig();
|
|
247
|
+
const event = makeEvent({
|
|
248
|
+
text: "<@U123BOT> <@ULEO> please look",
|
|
249
|
+
});
|
|
250
|
+
const userInfo = await resolveSlackUser("ULEO", "xoxb-test");
|
|
251
|
+
|
|
252
|
+
const result = normalizeSlackAppMention(
|
|
253
|
+
event,
|
|
254
|
+
"evt-mention-cache",
|
|
255
|
+
config,
|
|
256
|
+
"U123BOT",
|
|
257
|
+
undefined,
|
|
258
|
+
{ userLabels: userInfo ? { ULEO: userInfo.displayName } : {} },
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
expect(result).not.toBeNull();
|
|
262
|
+
expect(result!.event.message.content).toBe("@Leo please look");
|
|
263
|
+
expect(result!.event.message.content).not.toContain("<@ULEO>");
|
|
264
|
+
expect(result!.event.message.content).not.toContain("ULEO");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("renders unresolved mention IDs with fallback labels when lookup fails", async () => {
|
|
268
|
+
fetchMock = mock(async () => {
|
|
269
|
+
return new Response("", { status: 500 });
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const config = makeConfig();
|
|
273
|
+
const event = makeEvent({
|
|
274
|
+
text: "<@U123BOT> <@UFAIL> please look",
|
|
275
|
+
});
|
|
276
|
+
const userInfo = await resolveSlackUser("UFAIL", "xoxb-test");
|
|
277
|
+
|
|
278
|
+
const result = normalizeSlackAppMention(
|
|
279
|
+
event,
|
|
280
|
+
"evt-mention-fallback",
|
|
281
|
+
config,
|
|
282
|
+
"U123BOT",
|
|
283
|
+
undefined,
|
|
284
|
+
{ userLabels: userInfo ? { UFAIL: userInfo.displayName } : {} },
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
expect(result).not.toBeNull();
|
|
288
|
+
expect(result!.event.message.content).toBe("@unknown-user please look");
|
|
289
|
+
expect(result!.event.message.content).not.toContain("<@UFAIL>");
|
|
290
|
+
expect(result!.event.message.content).not.toContain("UFAIL");
|
|
291
|
+
});
|
|
292
|
+
|
|
229
293
|
test("omits displayName when bot token is not configured", () => {
|
|
230
294
|
const config = makeConfig();
|
|
231
295
|
const event = makeEvent();
|
|
@@ -247,6 +311,7 @@ describe("normalizeSlackAppMention with display name", () => {
|
|
|
247
311
|
event,
|
|
248
312
|
"evt-dn-3",
|
|
249
313
|
config,
|
|
314
|
+
undefined,
|
|
250
315
|
"xoxb-test",
|
|
251
316
|
);
|
|
252
317
|
|