@vellumai/vellum-gateway 0.3.20 → 0.3.22

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 CHANGED
@@ -25,6 +25,7 @@ Internet
25
25
  v
26
26
  Gateway (http://127.0.0.1:7830)
27
27
  |
28
+ +-- Dedicated /v1/health --> Runtime /v1/health
28
29
  +-- Runtime proxy /v1/* --> Runtime (http://127.0.0.1:7821)
29
30
  +-- /webhooks/* --> BLOCKED (404, never forwarded to runtime)
30
31
  ```
@@ -99,6 +100,60 @@ Guardian verification endpoints are exposed directly by the gateway and forwarde
99
100
  | `gateway/src/http/routes/guardian-control-plane-proxy.ts` | Guardian control-plane proxy handlers and upstream forwarding |
100
101
  | `gateway/src/index.ts` | Route registration and bearer-auth enforcement for `/v1/integrations/guardian/*` |
101
102
 
103
+ ### Runtime Health Proxy
104
+
105
+ Runtime health is exposed directly by the gateway at `GET /v1/health` and forwarded to the runtime's `GET /v1/health` endpoint even when the broad runtime proxy is disabled.
106
+
107
+ **Authentication boundary:**
108
+
109
+ - Gateway validates caller bearer auth against the runtime token.
110
+ - Gateway forwards the request to runtime with the runtime bearer token and `X-Gateway-Origin` proof header.
111
+ - Upstream 4xx/5xx responses are passed through, while connection errors return `502` and timeouts return `504`.
112
+
113
+ **Key source files:**
114
+
115
+ | File | Purpose |
116
+ |------|---------|
117
+ | `gateway/src/http/routes/runtime-health-proxy.ts` | Runtime health proxy handler and upstream forwarding |
118
+ | `gateway/src/index.ts` | Route registration and bearer-auth enforcement for `/v1/health` |
119
+
120
+ ### Telegram + Ingress Control-Plane Proxies
121
+
122
+ Telegram integration setup/config endpoints and ingress members/invites endpoints are also exposed directly by the gateway and forwarded to runtime handlers even when the broad runtime proxy is disabled.
123
+
124
+ **Forwarded Telegram endpoints:**
125
+
126
+ | Method | Path |
127
+ |--------|------|
128
+ | GET/POST/DELETE | `/v1/integrations/telegram/config` |
129
+ | POST | `/v1/integrations/telegram/commands` |
130
+ | POST | `/v1/integrations/telegram/setup` |
131
+
132
+ **Forwarded ingress endpoints:**
133
+
134
+ | Method | Path |
135
+ |--------|------|
136
+ | GET/POST | `/v1/ingress/members` |
137
+ | DELETE | `/v1/ingress/members/:memberId` |
138
+ | POST | `/v1/ingress/members/:memberId/block` |
139
+ | GET/POST | `/v1/ingress/invites` |
140
+ | DELETE | `/v1/ingress/invites/:inviteId` |
141
+ | POST | `/v1/ingress/invites/redeem` |
142
+
143
+ **Authentication boundary:**
144
+
145
+ - Gateway validates caller bearer auth against the runtime token.
146
+ - Gateway forwards requests to runtime with the runtime bearer token and `X-Gateway-Origin` proof header.
147
+ - Upstream 4xx/5xx responses are passed through, while connection errors return `502` and timeouts return `504`.
148
+
149
+ **Key source files:**
150
+
151
+ | File | Purpose |
152
+ |------|---------|
153
+ | `gateway/src/http/routes/telegram-control-plane-proxy.ts` | Telegram control-plane proxy handlers and upstream forwarding |
154
+ | `gateway/src/http/routes/ingress-control-plane-proxy.ts` | Ingress control-plane proxy handlers and upstream forwarding |
155
+ | `gateway/src/index.ts` | Route registration and bearer-auth enforcement for `/v1/integrations/telegram/*` and `/v1/ingress/*` |
156
+
102
157
  ### Channel Binding Lifecycle (Lane Separation)
103
158
 
104
159
  Each channel (desktop, Telegram, etc.) operates in its own **lane**: conversations created by an external channel are never displayed in the desktop thread list, and desktop conversations are never exposed to external channels. The `channelBinding` metadata on a conversation is used solely for routing inbound/outbound messages within that lane and for filtering sessions during desktop session restoration.
package/README.md CHANGED
@@ -221,6 +221,16 @@ The gateway serves as the single public ingress point for all external callbacks
221
221
  | `/v1/integrations/guardian/outbound/start` | POST | Authenticated control-plane proxy for starting outbound guardian verification |
222
222
  | `/v1/integrations/guardian/outbound/resend` | POST | Authenticated control-plane proxy for resending outbound guardian verification |
223
223
  | `/v1/integrations/guardian/outbound/cancel` | POST | Authenticated control-plane proxy for cancelling outbound guardian verification |
224
+ | `/v1/integrations/telegram/config` | GET/POST/DELETE | Authenticated control-plane proxy for Telegram integration config |
225
+ | `/v1/integrations/telegram/commands` | POST | Authenticated control-plane proxy for Telegram command registration |
226
+ | `/v1/integrations/telegram/setup` | POST | Authenticated control-plane proxy for Telegram setup orchestration |
227
+ | `/v1/ingress/members` | GET/POST | Authenticated control-plane proxy for listing/upserting ingress members |
228
+ | `/v1/ingress/members/:id` | DELETE | Authenticated control-plane proxy for revoking an ingress member |
229
+ | `/v1/ingress/members/:id/block` | POST | Authenticated control-plane proxy for blocking an ingress member |
230
+ | `/v1/ingress/invites` | GET/POST | Authenticated control-plane proxy for listing/creating ingress invites |
231
+ | `/v1/ingress/invites/:id` | DELETE | Authenticated control-plane proxy for revoking an ingress invite |
232
+ | `/v1/ingress/invites/redeem` | POST | Authenticated control-plane proxy for redeeming an ingress invite |
233
+ | `/v1/health` | GET | Authenticated runtime health proxy (`/v1/health` on runtime) |
224
234
  | `/healthz` | GET | Liveness probe |
225
235
  | `/readyz` | GET | Readiness probe |
226
236
  | `/schema` | GET | Returns the OpenAPI 3.1 schema for this gateway |
@@ -290,7 +300,7 @@ When `INGRESS_PUBLIC_BASE_URL` is configured, the gateway prioritizes it as the
290
300
 
291
301
  ## Default Mode: Dedicated Routes Only
292
302
 
293
- By default, the broad runtime proxy is disabled. Dedicated gateway-managed routes (webhooks, delivery endpoints, and explicit control-plane proxies such as `/v1/integrations/guardian/*`) remain available, but arbitrary runtime passthrough routes return `404` unless `GATEWAY_RUNTIME_PROXY_ENABLED=true`.
303
+ By default, the broad runtime proxy is disabled. Dedicated gateway-managed routes (webhooks, delivery endpoints, explicit control-plane proxies such as `/v1/integrations/guardian/*`, `/v1/integrations/telegram/*`, and `/v1/ingress/*`, plus the authenticated runtime health route `/v1/health`) remain available, but arbitrary runtime passthrough routes return `404` unless `GATEWAY_RUNTIME_PROXY_ENABLED=true`.
294
304
 
295
305
  ## Runtime Proxy Mode
296
306
 
@@ -340,6 +350,7 @@ Text and attachments are sent separately — the text reply goes first via `send
340
350
 
341
351
  | Endpoint | Method | Behavior |
342
352
  |----------|--------|----------|
353
+ | `/v1/health` | GET | Authenticated proxy to runtime health (`/v1/health`) |
343
354
  | `/healthz` | GET | Always returns `200` while the process is alive |
344
355
  | `/readyz` | GET | Returns `200` while accepting traffic; `503` during graceful shutdown drain |
345
356
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/vellum-gateway",
3
- "version": "0.3.20",
3
+ "version": "0.3.22",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "bun run --watch src/index.ts",
@@ -0,0 +1,180 @@
1
+ import { describe, test, expect, mock, afterEach } from "bun:test";
2
+ import type { GatewayConfig } from "../config.js";
3
+
4
+ type FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
5
+ let fetchMock: ReturnType<typeof mock<FetchFn>> = mock(async () => new Response());
6
+
7
+ mock.module("../fetch.js", () => ({
8
+ fetchImpl: (...args: Parameters<FetchFn>) => fetchMock(...args),
9
+ }));
10
+
11
+ const { createIngressControlPlaneProxyHandler } = await import(
12
+ "../http/routes/ingress-control-plane-proxy.js"
13
+ );
14
+
15
+ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
16
+ const merged: GatewayConfig = {
17
+ telegramBotToken: "tok",
18
+ telegramWebhookSecret: "wh-ver",
19
+ telegramApiBaseUrl: "https://api.telegram.org",
20
+ assistantRuntimeBaseUrl: "http://localhost:7821",
21
+ routingEntries: [],
22
+ defaultAssistantId: undefined,
23
+ unmappedPolicy: "reject",
24
+ port: 7830,
25
+ runtimeBearerToken: "runtime-token",
26
+ runtimeGatewayOriginSecret: "gateway-origin",
27
+ runtimeProxyEnabled: false,
28
+ runtimeProxyRequireAuth: true,
29
+ runtimeProxyBearerToken: undefined,
30
+ shutdownDrainMs: 5000,
31
+ runtimeTimeoutMs: 30000,
32
+ runtimeMaxRetries: 2,
33
+ runtimeInitialBackoffMs: 500,
34
+ telegramDeliverAuthBypass: false,
35
+ telegramInitialBackoffMs: 1000,
36
+ telegramMaxRetries: 3,
37
+ telegramTimeoutMs: 15000,
38
+ maxWebhookPayloadBytes: 1048576,
39
+ logFile: { dir: undefined, retentionDays: 30 },
40
+ maxAttachmentBytes: 20971520,
41
+ maxAttachmentConcurrency: 3,
42
+ twilioAuthToken: undefined,
43
+ twilioAccountSid: undefined,
44
+ twilioPhoneNumber: undefined,
45
+ smsDeliverAuthBypass: false,
46
+ ingressPublicBaseUrl: undefined,
47
+ gatewayInternalBaseUrl: "http://127.0.0.1:7830",
48
+ whatsappPhoneNumberId: undefined,
49
+ whatsappAccessToken: undefined,
50
+ whatsappAppSecret: undefined,
51
+ whatsappWebhookVerifyToken: undefined,
52
+ whatsappDeliverAuthBypass: false,
53
+ whatsappTimeoutMs: 15000,
54
+ whatsappMaxRetries: 3,
55
+ whatsappInitialBackoffMs: 1000,
56
+ slackChannelBotToken: undefined,
57
+ slackChannelAppToken: undefined,
58
+ slackDeliverAuthBypass: false,
59
+ trustProxy: false,
60
+ ...overrides,
61
+ };
62
+ if (merged.runtimeGatewayOriginSecret === undefined) {
63
+ merged.runtimeGatewayOriginSecret = merged.runtimeBearerToken;
64
+ }
65
+ return merged;
66
+ }
67
+
68
+ afterEach(() => {
69
+ fetchMock = mock(async () => new Response());
70
+ });
71
+
72
+ describe("ingress control-plane proxy", () => {
73
+ test("forwards ingress endpoints to the runtime", async () => {
74
+ const captured: string[] = [];
75
+ fetchMock = mock(async (input: string | URL | Request) => {
76
+ captured.push(String(input));
77
+ return new Response(JSON.stringify({ ok: true }), {
78
+ status: 200,
79
+ headers: { "content-type": "application/json" },
80
+ });
81
+ });
82
+
83
+ const handler = createIngressControlPlaneProxyHandler(makeConfig());
84
+
85
+ await handler.handleListMembers(
86
+ new Request("http://localhost:7830/v1/ingress/members?sourceChannel=telegram"),
87
+ );
88
+ await handler.handleUpsertMember(
89
+ new Request("http://localhost:7830/v1/ingress/members", { method: "POST" }),
90
+ );
91
+ await handler.handleRevokeMember(
92
+ new Request("http://localhost:7830/v1/ingress/members/mbr_123", { method: "DELETE" }),
93
+ "mbr_123",
94
+ );
95
+ await handler.handleBlockMember(
96
+ new Request("http://localhost:7830/v1/ingress/members/mbr_123/block", { method: "POST" }),
97
+ "mbr_123",
98
+ );
99
+ await handler.handleListInvites(
100
+ new Request("http://localhost:7830/v1/ingress/invites?status=active"),
101
+ );
102
+ await handler.handleCreateInvite(
103
+ new Request("http://localhost:7830/v1/ingress/invites", { method: "POST" }),
104
+ );
105
+ await handler.handleRedeemInvite(
106
+ new Request("http://localhost:7830/v1/ingress/invites/redeem", { method: "POST" }),
107
+ );
108
+ await handler.handleRevokeInvite(
109
+ new Request("http://localhost:7830/v1/ingress/invites/inv_123", { method: "DELETE" }),
110
+ "inv_123",
111
+ );
112
+
113
+ expect(captured).toEqual([
114
+ "http://localhost:7821/v1/ingress/members?sourceChannel=telegram",
115
+ "http://localhost:7821/v1/ingress/members",
116
+ "http://localhost:7821/v1/ingress/members/mbr_123",
117
+ "http://localhost:7821/v1/ingress/members/mbr_123/block",
118
+ "http://localhost:7821/v1/ingress/invites?status=active",
119
+ "http://localhost:7821/v1/ingress/invites",
120
+ "http://localhost:7821/v1/ingress/invites/redeem",
121
+ "http://localhost:7821/v1/ingress/invites/inv_123",
122
+ ]);
123
+ });
124
+
125
+ test("replaces caller auth with runtime auth and forwards gateway-origin proof", async () => {
126
+ let capturedHeaders: Headers | undefined;
127
+ fetchMock = mock(async (_input: string | URL | Request, init?: RequestInit) => {
128
+ capturedHeaders = init?.headers as unknown as Headers;
129
+ return new Response("ok", { status: 200 });
130
+ });
131
+
132
+ const handler = createIngressControlPlaneProxyHandler(makeConfig());
133
+ const res = await handler.handleUpsertMember(
134
+ new Request("http://localhost:7830/v1/ingress/members", {
135
+ method: "POST",
136
+ headers: {
137
+ authorization: "Bearer caller-token",
138
+ host: "localhost:7830",
139
+ },
140
+ body: JSON.stringify({ sourceChannel: "telegram", externalUserId: "u_1" }),
141
+ }),
142
+ );
143
+
144
+ expect(res.status).toBe(200);
145
+ expect(capturedHeaders?.get("authorization")).toBe("Bearer runtime-token");
146
+ expect(capturedHeaders?.get("X-Gateway-Origin")).toBe("gateway-origin");
147
+ expect(capturedHeaders?.has("host")).toBe(false);
148
+ });
149
+
150
+ test("passes through upstream client errors", async () => {
151
+ fetchMock = mock(async () => {
152
+ return new Response(JSON.stringify({ ok: false, error: "sourceChannel is required" }), {
153
+ status: 400,
154
+ headers: { "content-type": "application/json" },
155
+ });
156
+ });
157
+
158
+ const handler = createIngressControlPlaneProxyHandler(makeConfig());
159
+ const res = await handler.handleCreateInvite(
160
+ new Request("http://localhost:7830/v1/ingress/invites", { method: "POST" }),
161
+ );
162
+
163
+ expect(res.status).toBe(400);
164
+ expect(await res.json()).toEqual({ ok: false, error: "sourceChannel is required" });
165
+ });
166
+
167
+ test("returns 504 when upstream times out", async () => {
168
+ fetchMock = mock(async () => {
169
+ throw new DOMException("The operation was aborted due to timeout", "TimeoutError");
170
+ });
171
+
172
+ const handler = createIngressControlPlaneProxyHandler(makeConfig({ runtimeTimeoutMs: 100 }));
173
+ const res = await handler.handleListMembers(
174
+ new Request("http://localhost:7830/v1/ingress/members"),
175
+ );
176
+
177
+ expect(res.status).toBe(504);
178
+ expect(await res.json()).toEqual({ error: "Gateway Timeout" });
179
+ });
180
+ });
@@ -0,0 +1,53 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { matchIngressControlPlaneRoute } from "../http/routes/ingress-control-plane-route-match.js";
3
+
4
+ describe("matchIngressControlPlaneRoute", () => {
5
+ test("matches redeem invite only for POST", () => {
6
+ expect(matchIngressControlPlaneRoute("/v1/ingress/invites/redeem", "POST")).toEqual({
7
+ kind: "redeemInvite",
8
+ });
9
+
10
+ // DELETE should treat `redeem` as an invite ID so revoke routing still works.
11
+ expect(matchIngressControlPlaneRoute("/v1/ingress/invites/redeem", "DELETE")).toEqual({
12
+ kind: "revokeInvite",
13
+ inviteId: "redeem",
14
+ });
15
+ });
16
+
17
+ test("matches ingress member routes", () => {
18
+ expect(matchIngressControlPlaneRoute("/v1/ingress/members", "GET")).toEqual({
19
+ kind: "listMembers",
20
+ });
21
+ expect(matchIngressControlPlaneRoute("/v1/ingress/members", "POST")).toEqual({
22
+ kind: "upsertMember",
23
+ });
24
+ expect(matchIngressControlPlaneRoute("/v1/ingress/members/mbr_1/block", "POST")).toEqual({
25
+ kind: "blockMember",
26
+ memberId: "mbr_1",
27
+ });
28
+ expect(matchIngressControlPlaneRoute("/v1/ingress/members/mbr_1", "DELETE")).toEqual({
29
+ kind: "revokeMember",
30
+ memberId: "mbr_1",
31
+ });
32
+ });
33
+
34
+ test("matches ingress invite routes", () => {
35
+ expect(matchIngressControlPlaneRoute("/v1/ingress/invites", "GET")).toEqual({
36
+ kind: "listInvites",
37
+ });
38
+ expect(matchIngressControlPlaneRoute("/v1/ingress/invites", "POST")).toEqual({
39
+ kind: "createInvite",
40
+ });
41
+ expect(matchIngressControlPlaneRoute("/v1/ingress/invites/inv_1", "DELETE")).toEqual({
42
+ kind: "revokeInvite",
43
+ inviteId: "inv_1",
44
+ });
45
+ });
46
+
47
+ test("returns null for unsupported method/path combinations", () => {
48
+ expect(matchIngressControlPlaneRoute("/v1/ingress/invites/redeem", "GET")).toBeNull();
49
+ expect(matchIngressControlPlaneRoute("/v1/ingress/invites/inv_1", "POST")).toBeNull();
50
+ expect(matchIngressControlPlaneRoute("/v1/ingress/members/mbr_1", "POST")).toBeNull();
51
+ expect(matchIngressControlPlaneRoute("/v1/ingress/unknown", "GET")).toBeNull();
52
+ });
53
+ });
@@ -0,0 +1,129 @@
1
+ import { describe, test, expect, mock, afterEach } from "bun:test";
2
+ import type { GatewayConfig } from "../config.js";
3
+
4
+ type FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
5
+ let fetchMock: ReturnType<typeof mock<FetchFn>> = mock(async () => new Response());
6
+
7
+ mock.module("../fetch.js", () => ({
8
+ fetchImpl: (...args: Parameters<FetchFn>) => fetchMock(...args),
9
+ }));
10
+
11
+ const { createRuntimeHealthProxyHandler } = await import(
12
+ "../http/routes/runtime-health-proxy.js"
13
+ );
14
+
15
+ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
16
+ const merged: GatewayConfig = {
17
+ telegramBotToken: "tok",
18
+ telegramWebhookSecret: "wh-ver",
19
+ telegramApiBaseUrl: "https://api.telegram.org",
20
+ assistantRuntimeBaseUrl: "http://localhost:7821",
21
+ routingEntries: [],
22
+ defaultAssistantId: undefined,
23
+ unmappedPolicy: "reject",
24
+ port: 7830,
25
+ runtimeBearerToken: "runtime-token",
26
+ runtimeGatewayOriginSecret: "gateway-origin",
27
+ runtimeProxyEnabled: false,
28
+ runtimeProxyRequireAuth: true,
29
+ runtimeProxyBearerToken: undefined,
30
+ shutdownDrainMs: 5000,
31
+ runtimeTimeoutMs: 30000,
32
+ runtimeMaxRetries: 2,
33
+ runtimeInitialBackoffMs: 500,
34
+ telegramDeliverAuthBypass: false,
35
+ telegramInitialBackoffMs: 1000,
36
+ telegramMaxRetries: 3,
37
+ telegramTimeoutMs: 15000,
38
+ maxWebhookPayloadBytes: 1048576,
39
+ logFile: { dir: undefined, retentionDays: 30 },
40
+ maxAttachmentBytes: 20971520,
41
+ maxAttachmentConcurrency: 3,
42
+ twilioAuthToken: undefined,
43
+ twilioAccountSid: undefined,
44
+ twilioPhoneNumber: undefined,
45
+ smsDeliverAuthBypass: false,
46
+ ingressPublicBaseUrl: undefined,
47
+ gatewayInternalBaseUrl: "http://127.0.0.1:7830",
48
+ whatsappPhoneNumberId: undefined,
49
+ whatsappAccessToken: undefined,
50
+ whatsappAppSecret: undefined,
51
+ whatsappWebhookVerifyToken: undefined,
52
+ whatsappDeliverAuthBypass: false,
53
+ whatsappTimeoutMs: 15000,
54
+ whatsappMaxRetries: 3,
55
+ whatsappInitialBackoffMs: 1000,
56
+ slackChannelBotToken: undefined,
57
+ slackChannelAppToken: undefined,
58
+ slackDeliverAuthBypass: false,
59
+ trustProxy: false,
60
+ ...overrides,
61
+ };
62
+ if (merged.runtimeGatewayOriginSecret === undefined) {
63
+ merged.runtimeGatewayOriginSecret = merged.runtimeBearerToken;
64
+ }
65
+ return merged;
66
+ }
67
+
68
+ afterEach(() => {
69
+ fetchMock = mock(async () => new Response());
70
+ });
71
+
72
+ describe("runtime health proxy", () => {
73
+ test("forwards to runtime /v1/health", async () => {
74
+ const captured: string[] = [];
75
+ fetchMock = mock(async (input: string | URL | Request) => {
76
+ captured.push(String(input));
77
+ return new Response(JSON.stringify({ status: "healthy" }), {
78
+ status: 200,
79
+ headers: { "content-type": "application/json" },
80
+ });
81
+ });
82
+
83
+ const handler = createRuntimeHealthProxyHandler(makeConfig());
84
+ const res = await handler.handleRuntimeHealth(
85
+ new Request("http://localhost:7830/v1/health"),
86
+ );
87
+
88
+ expect(res.status).toBe(200);
89
+ expect(captured).toEqual(["http://localhost:7821/v1/health"]);
90
+ expect(await res.json()).toEqual({ status: "healthy" });
91
+ });
92
+
93
+ test("replaces caller auth with runtime auth and forwards gateway-origin proof", async () => {
94
+ let capturedHeaders: Headers | undefined;
95
+ fetchMock = mock(async (_input: string | URL | Request, init?: RequestInit) => {
96
+ capturedHeaders = init?.headers as unknown as Headers;
97
+ return new Response("ok", { status: 200 });
98
+ });
99
+
100
+ const handler = createRuntimeHealthProxyHandler(makeConfig());
101
+ const res = await handler.handleRuntimeHealth(
102
+ new Request("http://localhost:7830/v1/health", {
103
+ headers: {
104
+ authorization: "Bearer caller-token",
105
+ host: "localhost:7830",
106
+ },
107
+ }),
108
+ );
109
+
110
+ expect(res.status).toBe(200);
111
+ expect(capturedHeaders?.get("authorization")).toBe("Bearer runtime-token");
112
+ expect(capturedHeaders?.get("X-Gateway-Origin")).toBe("gateway-origin");
113
+ expect(capturedHeaders?.has("host")).toBe(false);
114
+ });
115
+
116
+ test("returns 504 on timeout", async () => {
117
+ fetchMock = mock(async () => {
118
+ throw new DOMException("The operation was aborted due to timeout", "TimeoutError");
119
+ });
120
+
121
+ const handler = createRuntimeHealthProxyHandler(makeConfig({ runtimeTimeoutMs: 100 }));
122
+ const res = await handler.handleRuntimeHealth(
123
+ new Request("http://localhost:7830/v1/health"),
124
+ );
125
+
126
+ expect(res.status).toBe(504);
127
+ expect(await res.json()).toEqual({ error: "Gateway Timeout" });
128
+ });
129
+ });
@@ -54,12 +54,22 @@ describe("/schema route", () => {
54
54
  expect(body.paths["/healthz"]).toBeDefined();
55
55
  expect(body.paths["/readyz"]).toBeDefined();
56
56
  expect(body.paths["/schema"]).toBeDefined();
57
+ expect(body.paths["/v1/health"]).toBeDefined();
57
58
  expect(body.paths["/webhooks/telegram"]).toBeDefined();
58
59
  expect(body.paths["/webhooks/twilio/voice"]).toBeDefined();
59
60
  expect(body.paths["/webhooks/twilio/status"]).toBeDefined();
60
61
  expect(body.paths["/webhooks/twilio/connect-action"]).toBeDefined();
61
62
  expect(body.paths["/webhooks/twilio/relay"]).toBeDefined();
62
63
  expect(body.paths["/webhooks/oauth/callback"]).toBeDefined();
64
+ expect(body.paths["/v1/integrations/telegram/config"]).toBeDefined();
65
+ expect(body.paths["/v1/integrations/telegram/commands"]).toBeDefined();
66
+ expect(body.paths["/v1/integrations/telegram/setup"]).toBeDefined();
67
+ expect(body.paths["/v1/ingress/members"]).toBeDefined();
68
+ expect(body.paths["/v1/ingress/members/{memberId}"]).toBeDefined();
69
+ expect(body.paths["/v1/ingress/members/{memberId}/block"]).toBeDefined();
70
+ expect(body.paths["/v1/ingress/invites"]).toBeDefined();
71
+ expect(body.paths["/v1/ingress/invites/redeem"]).toBeDefined();
72
+ expect(body.paths["/v1/ingress/invites/{inviteId}"]).toBeDefined();
63
73
  expect(body.paths["/v1/integrations/guardian/challenge"]).toBeDefined();
64
74
  expect(body.paths["/v1/integrations/guardian/status"]).toBeDefined();
65
75
  expect(body.paths["/v1/integrations/guardian/outbound/start"]).toBeDefined();
@@ -147,4 +157,20 @@ describe("buildSchema()", () => {
147
157
  });
148
158
  expect(telegramDeliver.anyOf).toContainEqual({ required: ["chatAction"] });
149
159
  });
160
+
161
+ test("ingress member block request body is required", () => {
162
+ const schema = buildSchema() as {
163
+ paths: Record<string, {
164
+ post?: {
165
+ requestBody?: {
166
+ required?: boolean;
167
+ };
168
+ };
169
+ }>;
170
+ };
171
+
172
+ const ingressMemberBlockPost = schema.paths["/v1/ingress/members/{memberId}/block"]?.post;
173
+ expect(ingressMemberBlockPost).toBeDefined();
174
+ expect(ingressMemberBlockPost?.requestBody?.required).toBe(true);
175
+ });
150
176
  });