@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.
Files changed (162) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +67 -25
  3. package/Dockerfile +2 -0
  4. package/README.md +50 -13
  5. package/bun.lock +16 -2
  6. package/knip.json +3 -1
  7. package/package.json +3 -1
  8. package/src/__tests__/auto-approve-thresholds.test.ts +49 -22
  9. package/src/__tests__/channel-verification-session-proxy.test.ts +0 -1
  10. package/src/__tests__/config-file-watcher.test.ts +181 -0
  11. package/src/__tests__/config.test.ts +0 -1
  12. package/src/__tests__/contacts-control-plane-proxy.test.ts +0 -1
  13. package/src/__tests__/credential-watcher-managed-bootstrap.test.ts +10 -2
  14. package/src/__tests__/credential-watcher.test.ts +30 -2
  15. package/src/__tests__/db-connection-isolation.test.ts +157 -0
  16. package/src/__tests__/fake-assistant-ipc.ts +39 -0
  17. package/src/__tests__/feature-flags-route.test.ts +8 -8
  18. package/src/__tests__/guardian-init-lockfile.test.ts +30 -4
  19. package/src/__tests__/ipc-feature-flag-routes.test.ts +1 -1
  20. package/src/__tests__/live-voice-websocket.test.ts +0 -1
  21. package/src/__tests__/load-guards.test.ts +0 -1
  22. package/src/__tests__/migration-teleport-gcs-proxy.test.ts +0 -1
  23. package/src/__tests__/oauth-callback.test.ts +0 -1
  24. package/src/__tests__/pair-origin-allowlist.test.ts +155 -0
  25. package/src/__tests__/rate-limit-loopback.test.ts +1 -1
  26. package/src/__tests__/remote-feature-flag-sync.test.ts +47 -7
  27. package/src/__tests__/resolve-assistant.test.ts +0 -1
  28. package/src/__tests__/route-schema-guard.test.ts +42 -6
  29. package/src/__tests__/runtime-client.test.ts +0 -1
  30. package/src/__tests__/runtime-health-proxy.test.ts +0 -1
  31. package/src/__tests__/runtime-proxy-auth.test.ts +0 -1
  32. package/src/__tests__/runtime-proxy.test.ts +0 -1
  33. package/src/__tests__/slack-control-plane-proxy.test.ts +0 -1
  34. package/src/__tests__/slack-display-name.test.ts +66 -1
  35. package/src/__tests__/slack-normalize.test.ts +158 -4
  36. package/src/__tests__/slack-reaction-normalize.test.ts +0 -1
  37. package/src/__tests__/slack-socket-mode-catchup.test.ts +857 -0
  38. package/src/__tests__/slack-socket-mode-scopes.test.ts +52 -0
  39. package/src/__tests__/slack-socket-mode-thread-tracking.test.ts +654 -0
  40. package/src/__tests__/stt-stream-websocket.test.ts +0 -1
  41. package/src/__tests__/telegram-control-plane-proxy.test.ts +0 -1
  42. package/src/__tests__/telegram-send-attachments.test.ts +0 -1
  43. package/src/__tests__/telegram-webhook-handler.test.ts +0 -1
  44. package/src/__tests__/text-verification-helpers.test.ts +136 -0
  45. package/src/__tests__/twilio-media-websocket.test.ts +0 -1
  46. package/src/__tests__/twilio-relay-websocket.test.ts +0 -1
  47. package/src/__tests__/twilio-webhooks.test.ts +220 -3
  48. package/src/__tests__/upstream-transport.test.ts +0 -36
  49. package/src/__tests__/whatsapp-download.test.ts +0 -1
  50. package/src/__tests__/whatsapp-webhook.test.ts +0 -1
  51. package/src/auth/guardian-refresh.ts +4 -18
  52. package/src/auth/ipc-route-policy.ts +217 -0
  53. package/src/backup/backup-key.ts +138 -0
  54. package/src/backup/backup-routes.ts +159 -0
  55. package/src/backup/backup-worker.ts +374 -0
  56. package/src/backup/list-snapshots.ts +97 -0
  57. package/src/backup/local-writer.ts +87 -0
  58. package/src/backup/offsite-writer.ts +182 -0
  59. package/src/backup/paths.ts +123 -0
  60. package/src/backup/stream-crypt.ts +258 -0
  61. package/src/chrome-extension-origins.ts +28 -0
  62. package/src/cli/enable-proxy.ts +0 -1
  63. package/src/config-file-cache.ts +3 -19
  64. package/src/config-file-utils.ts +124 -0
  65. package/src/config-file-watcher.ts +57 -25
  66. package/src/config.ts +4 -7
  67. package/src/db/connection.ts +65 -3
  68. package/src/db/contact-store.ts +30 -1
  69. package/src/db/data-migrations/index.ts +2 -0
  70. package/src/db/data-migrations/m0003-recover-backup-key.ts +71 -0
  71. package/src/db/schema.ts +92 -0
  72. package/src/db/slack-store.ts +144 -11
  73. package/src/feature-flag-registry.json +40 -152
  74. package/src/handlers/handle-inbound.ts +123 -0
  75. package/src/http/middleware/auth.ts +44 -1
  76. package/src/http/middleware/cors.ts +84 -0
  77. package/src/http/middleware/rate-limit.ts +6 -8
  78. package/src/http/routes/auto-approve-thresholds.ts +17 -1
  79. package/src/http/routes/brain-graph-proxy.ts +1 -1
  80. package/src/http/routes/channel-readiness-proxy.ts +2 -2
  81. package/src/http/routes/channel-verification-session-proxy.ts +19 -37
  82. package/src/http/routes/contact-prompt.ts +149 -0
  83. package/src/http/routes/contacts-control-plane-proxy.ts +2 -2
  84. package/src/http/routes/email-webhook.test.ts +0 -1
  85. package/src/http/routes/ipc-runtime-proxy.test.ts +197 -1
  86. package/src/http/routes/ipc-runtime-proxy.ts +95 -0
  87. package/src/http/routes/log-export.test.ts +0 -1
  88. package/src/http/routes/log-tail.test.ts +336 -0
  89. package/src/http/routes/log-tail.ts +87 -0
  90. package/src/http/routes/migration-proxy.ts +1 -2
  91. package/src/http/routes/oauth-apps-proxy.ts +2 -2
  92. package/src/http/routes/oauth-providers-proxy.ts +2 -2
  93. package/src/http/routes/pair.ts +322 -0
  94. package/src/http/routes/privacy-config.ts +65 -79
  95. package/src/http/routes/runtime-health-proxy.ts +2 -2
  96. package/src/http/routes/runtime-proxy.ts +3 -1
  97. package/src/http/routes/slack-control-plane-proxy.ts +3 -20
  98. package/src/http/routes/stt-stream-websocket.ts +2 -3
  99. package/src/http/routes/telegram-control-plane-proxy.ts +2 -2
  100. package/src/http/routes/telegram-webhook.test.ts +0 -1
  101. package/src/http/routes/telegram-webhook.ts +6 -0
  102. package/src/http/routes/trust-rules.suggest.test.ts +25 -0
  103. package/src/http/routes/trust-rules.ts +7 -0
  104. package/src/http/routes/twilio-control-plane-proxy.ts +2 -2
  105. package/src/http/routes/twilio-media-websocket.ts +5 -5
  106. package/src/http/routes/twilio-voice-verify-callback.ts +310 -0
  107. package/src/http/routes/twilio-voice-webhook.test.ts +65 -1
  108. package/src/http/routes/twilio-voice-webhook.ts +45 -1
  109. package/src/http/routes/whatsapp-webhook.test.ts +0 -1
  110. package/src/index.ts +357 -278
  111. package/src/ipc/assistant-client.ts +8 -4
  112. package/src/ipc/contact-handlers.ts +88 -3
  113. package/src/ipc/threshold-handlers.ts +2 -0
  114. package/src/post-assistant-ready.ts +5 -3
  115. package/src/risk/bash-risk-classifier.test.ts +35 -27
  116. package/src/risk/bash-risk-classifier.ts +44 -14
  117. package/src/risk/command-registry/commands/assistant.ts +8 -19
  118. package/src/risk/command-registry.test.ts +0 -15
  119. package/src/risk/risk-classifier-parity.test.ts +1 -3
  120. package/src/runtime/client.ts +58 -3
  121. package/src/schema.ts +277 -104
  122. package/src/slack/normalize.test.ts +98 -0
  123. package/src/slack/normalize.ts +107 -32
  124. package/src/slack/slack-web.ts +213 -0
  125. package/src/slack/socket-mode.ts +701 -39
  126. package/src/telegram/send.test.ts +0 -1
  127. package/src/twilio/validate-webhook.ts +53 -14
  128. package/src/twilio/webhook-sync-trigger.ts +58 -0
  129. package/src/twilio/webhook-sync.test.ts +286 -0
  130. package/src/twilio/webhook-sync.ts +84 -0
  131. package/src/util/is-loopback-address.ts +27 -0
  132. package/src/velay/bridge-utils.ts +228 -0
  133. package/src/velay/client.test.ts +939 -0
  134. package/src/velay/client.ts +555 -0
  135. package/src/velay/http-bridge.test.ts +217 -0
  136. package/src/velay/http-bridge.ts +83 -0
  137. package/src/velay/protocol.ts +178 -0
  138. package/src/velay/test-fake-websocket.ts +69 -0
  139. package/src/velay/websocket-bridge.test.ts +367 -0
  140. package/src/velay/websocket-bridge.ts +324 -0
  141. package/src/verification/binding-helpers.ts +107 -0
  142. package/src/verification/code-parsing.ts +44 -0
  143. package/src/verification/contact-helpers.ts +342 -0
  144. package/src/verification/identity-match.ts +68 -0
  145. package/src/verification/identity.ts +61 -0
  146. package/src/verification/rate-limit-helpers.ts +205 -0
  147. package/src/verification/reply-delivery.ts +109 -0
  148. package/src/verification/session-helpers.ts +164 -0
  149. package/src/verification/text-verification.ts +372 -0
  150. package/src/version.ts +35 -0
  151. package/src/voice/verification.ts +456 -0
  152. package/src/webhook-pipeline.ts +4 -0
  153. package/src/__tests__/browser-relay-websocket.test.ts +0 -698
  154. package/src/__tests__/telegram-only-default.test.ts +0 -133
  155. package/src/auth/capability-tokens.ts +0 -248
  156. package/src/http/routes/browser-extension-pair.ts +0 -455
  157. package/src/http/routes/browser-relay-websocket.ts +0 -381
  158. package/src/http/routes/config-file-utils.ts +0 -73
  159. package/src/ipc/capability-token-handlers.ts +0 -30
  160. package/src/pairing/approved-devices-store.ts +0 -110
  161. package/src/pairing/pairing-routes.ts +0 -379
  162. 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/v1/browser-relay"),
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
- "conversation-starters": false,
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
- browser: true,
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
- // conversation-starters (GA, remote false) should be absent
450
- expect(cached["conversation-starters"]).toBeUndefined();
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
- // browser (GA, remote true) should be present
454
- expect(cached.browser).toBe(true);
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