@vellumai/vellum-gateway 0.4.16 → 0.4.18

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
@@ -326,7 +326,7 @@ Runtime detects needs_confirmation
326
326
 
327
327
  **Proactive expiry sweep:** The runtime runs a periodic sweep every 60 seconds (`sweepExpiredGuardianApprovals`) that finds guardian approval requests past the 30-minute TTL, auto-denies the underlying runs, and notifies both the requester and guardian via the gateway's per-channel `/deliver/<channel>` endpoint. This ensures expired approvals are closed without waiting for follow-up traffic from either party. The sweep is started automatically whenever a run orchestrator is available.
328
328
 
329
- **Gateway-origin ingress contract:** The `/channels/inbound` endpoint requires a valid `X-Gateway-Origin` header to prove the request originated from the gateway. When `RUNTIME_GATEWAY_ORIGIN_SECRET` is set, it is the expected header value. When not set, the runtime falls back to the bearer token. When neither is configured (local dev), validation is skipped. The gateway sends this header on all runtime-bound requests via its `runtimeHeaders()` helper. Constant-time comparison prevents timing attacks.
329
+ **Gateway-origin ingress contract:** The JWT token exchanged during gateway-to-runtime authentication proves gateway origin (via the `aud=vellum-daemon` claim). No separate header is required.
330
330
 
331
331
  **Key modules:**
332
332
 
@@ -395,8 +395,8 @@ sequenceDiagram
395
395
  User->>TG: <replies with code>
396
396
  TG->>GW: POST /webhooks/telegram (webhook secret validated)
397
397
  GW->>GW: Verify webhook secret, normalize update
398
- GW->>Daemon: POST /v1/channels/inbound (X-Gateway-Origin proof)
399
- Daemon->>Daemon: Verify gateway-origin proof
398
+ GW->>Daemon: POST /v1/channels/inbound (JWT auth)
399
+ Daemon->>Daemon: Verify JWT auth
400
400
  Daemon->>Daemon: Hash secret, find pending challenge, validate expiry
401
401
  Daemon->>Daemon: Consume challenge (replay prevention)
402
402
  Daemon->>Daemon: Revoke existing binding (if any)
@@ -413,8 +413,8 @@ The channel inbound handler (`inbound-message-handler.ts`) evaluates incoming me
413
413
 
414
414
  ```mermaid
415
415
  flowchart TD
416
- MSG["Inbound message arrives<br/>POST /channels/inbound"] --> GW_CHECK{"Gateway-origin<br/>proof valid?"}
417
- GW_CHECK -- No --> REJECT_403["403 GATEWAY_ORIGIN_REQUIRED"]
416
+ MSG["Inbound message arrives<br/>POST /channels/inbound"] --> GW_CHECK{"JWT auth<br/>valid?"}
417
+ GW_CHECK -- No --> REJECT_403["403 Unauthorized"]
418
418
  GW_CHECK -- Yes --> HAS_SENDER{"actorExternalId<br/>present?"}
419
419
 
420
420
  HAS_SENDER -- Yes --> ACL_LOOKUP["Look up ingress member<br/>by (channel, userId/chatId)"]
@@ -465,7 +465,7 @@ sequenceDiagram
465
465
 
466
466
  NG->>TG: Message triggers tool use
467
467
  TG->>GW: POST /webhooks/telegram
468
- GW->>Daemon: POST /v1/channels/inbound (X-Gateway-Origin proof)
468
+ GW->>Daemon: POST /v1/channels/inbound (JWT auth)
469
469
  Daemon->>Daemon: Detect non-guardian, set forceStrictSideEffects
470
470
  Daemon->>Daemon: Tool needs confirmation → create GuardianApprovalRequest
471
471
  Daemon->>GW: POST /deliver/telegram (approval prompt + inline keyboard)
@@ -474,7 +474,7 @@ sequenceDiagram
474
474
  GW-->>NG: "Waiting for guardian approval..."
475
475
  Guardian->>TG: Approve / Deny (callback_query or text)
476
476
  TG->>GW: POST /webhooks/telegram (callback_query)
477
- GW->>Daemon: POST /v1/channels/inbound (X-Gateway-Origin proof)
477
+ GW->>Daemon: POST /v1/channels/inbound (JWT auth)
478
478
  Daemon->>Daemon: Validate guardian identity, update approval decision
479
479
  Daemon->>Daemon: Apply decision to pending run
480
480
  Daemon->>GW: POST /deliver/telegram (outcome notification)
package/README.md CHANGED
@@ -41,7 +41,6 @@ bun run dev
41
41
  | `GATEWAY_RUNTIME_PROXY_ENABLED` | No | `false` | Enable runtime proxy for non-Telegram requests |
42
42
  | `GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH` | No | `true` | Require bearer auth for proxied requests |
43
43
  | `RUNTIME_BEARER_TOKEN` | No | `~/.vellum/http-token` (if present) | Bearer token used by gateway when forwarding requests to assistant runtime internal endpoints (Twilio/OAuth/proxy upstream). |
44
- | `RUNTIME_GATEWAY_ORIGIN_SECRET` | No | Falls back to `RUNTIME_BEARER_TOKEN` | Dedicated secret sent as the `X-Gateway-Origin` header on `/channels/inbound` requests to prove gateway origin. When not set, the gateway falls back to sending `RUNTIME_BEARER_TOKEN` as the origin proof. Both gateway and runtime must share the same value. |
45
44
  | `RUNTIME_PROXY_BEARER_TOKEN` | Conditional | — | Bearer token for proxy auth (required when proxy + auth enabled) |
46
45
  | `GATEWAY_SHUTDOWN_DRAIN_MS` | No | `5000` | Graceful shutdown drain window in milliseconds |
47
46
  | `GATEWAY_RUNTIME_TIMEOUT_MS` | No | `30000` | Timeout for runtime HTTP calls (ms) |
@@ -291,7 +290,7 @@ The gateway is the **sole public ingress point** for all external webhooks, incl
291
290
  Inbound SMS follows the same gateway-only pattern as voice and Telegram:
292
291
 
293
292
  1. **Twilio → Gateway** (`/webhooks/twilio/sms`) — Gateway validates `X-Twilio-Signature` using HMAC-SHA1 with the configured `TWILIO_AUTH_TOKEN`.
294
- 2. **Gateway → Runtime** (`/v1/channels/inbound`) — Gateway forwards the normalized event to the runtime with `X-Gateway-Origin` proof and bearer auth.
293
+ 2. **Gateway → Runtime** (`/v1/channels/inbound`) — Gateway forwards the normalized event to the runtime with JWT bearer auth.
295
294
  3. **Runtime rejects direct SMS webhooks** — Any direct POST to `/webhooks/twilio/sms` or `/v1/calls/twilio/sms` on the runtime returns `410 GATEWAY_ONLY`.
296
295
 
297
296
  ### Signature URL Tightening
@@ -413,7 +412,7 @@ See [`benchmarking/gateway/README.md`](../benchmarking/gateway/README.md) for lo
413
412
  | "No route configured" replies | Add a routing entry or set `GATEWAY_UNMAPPED_POLICY=default` with a default assistant |
414
413
  | Runtime errors | Is `ASSISTANT_RUNTIME_BASE_URL` reachable? Check runtime logs. |
415
414
  | No reply from assistant | Is the assistant runtime processing messages? Check for `RUNTIME_HTTP_PORT` env var. |
416
- | 403 `GATEWAY_ORIGIN_REQUIRED` on channel inbound | The runtime rejected the request because it lacks a valid `X-Gateway-Origin` header. Ensure `RUNTIME_GATEWAY_ORIGIN_SECRET` (or `RUNTIME_BEARER_TOKEN` / `~/.vellum/http-token` as fallback) is set on both the gateway and runtime so they share the same secret. |
415
+ | 403 on channel inbound | The runtime rejected the request because JWT authentication failed. Ensure the gateway and runtime share the same signing key (`~/.vellum/protected/actor-token-signing-key`). |
417
416
 
418
417
  ### Guardian-Specific Troubleshooting
419
418
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/vellum-gateway",
3
- "version": "0.4.16",
3
+ "version": "0.4.18",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./twilio/verify": "./src/twilio/verify.ts",
@@ -1,10 +1,26 @@
1
1
  import { describe, test, expect, mock, beforeEach, afterAll } from "bun:test";
2
2
  import type { GatewayConfig } from "../config.js";
3
+ import { initSigningKey, mintToken } from "../auth/token-service.js";
4
+ import { CURRENT_POLICY_EPOCH } from "../auth/policy.js";
3
5
  import {
4
6
  createBrowserRelayWebsocketHandler,
5
7
  getBrowserRelayWebsocketHandlers,
6
8
  } from "../http/routes/browser-relay-websocket.js";
7
9
 
10
+ const TEST_SIGNING_KEY = Buffer.from('test-signing-key-at-least-32-bytes-long');
11
+ initSigningKey(TEST_SIGNING_KEY);
12
+
13
+ /** Mint a valid edge JWT for browser relay auth. */
14
+ function mintEdgeToken(): string {
15
+ return mintToken({
16
+ aud: 'vellum-gateway',
17
+ sub: 'actor:test-assistant:test-user',
18
+ scope_profile: 'actor_client_v1',
19
+ policy_epoch: CURRENT_POLICY_EPOCH,
20
+ ttlSeconds: 300,
21
+ });
22
+ }
23
+
8
24
  const WS_CONNECTING = WebSocket.CONNECTING; // 0
9
25
  const WS_OPEN = WebSocket.OPEN; // 1
10
26
  const WS_CLOSED = WebSocket.CLOSED; // 3
@@ -95,7 +111,7 @@ function createFakeUpstreamWs() {
95
111
  }
96
112
 
97
113
  describe("createBrowserRelayWebsocketHandler", () => {
98
- const TEST_TOKEN = "relay-token-abc123";
114
+ const TEST_TOKEN = mintEdgeToken();
99
115
 
100
116
  test("upgrades when token query parameter is valid", () => {
101
117
  const config = makeConfig({});
@@ -238,7 +254,8 @@ describe("getBrowserRelayWebsocketHandlers", () => {
238
254
  handlers.message(ws as never, "hello-before-open");
239
255
 
240
256
  const MockWS = globalThis.WebSocket as unknown as ReturnType<typeof mock>;
241
- expect(MockWS).toHaveBeenCalledWith("ws://runtime.internal:7821/v1/browser-relay?token=runtime-token");
257
+ const calledUrl = (MockWS.mock.calls[0] as unknown[])[0] as string;
258
+ expect(calledUrl).toMatch(/^ws:\/\/runtime\.internal:7821\/v1\/browser-relay\?token=ey/);
242
259
 
243
260
  fakeUpstream.readyState = WS_OPEN;
244
261
  fakeUpstream.emit("open");
@@ -1,4 +1,4 @@
1
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync, unlinkSync } from "node:fs";
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { describe, test, expect } from "bun:test";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
@@ -1,5 +1,9 @@
1
1
  import { describe, test, expect, mock, afterEach } from "bun:test";
2
2
  import type { GatewayConfig } from "../config.js";
3
+ import { initSigningKey } from "../auth/token-service.js";
4
+
5
+ const TEST_SIGNING_KEY = Buffer.from('test-signing-key-at-least-32-bytes-long');
6
+ initSigningKey(TEST_SIGNING_KEY);
3
7
 
4
8
  type FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
5
9
  let fetchMock: ReturnType<typeof mock<FetchFn>> = mock(async () => new Response());
@@ -101,7 +105,7 @@ describe("guardian control-plane proxy", () => {
101
105
  ]);
102
106
  });
103
107
 
104
- test("replaces caller auth with runtime auth and forwards gateway-origin proof", async () => {
108
+ test("replaces caller auth with runtime auth", async () => {
105
109
  let capturedHeaders: Headers | undefined;
106
110
  let capturedBody = "";
107
111
  fetchMock = mock(async (_input: string | URL | Request, init?: RequestInit) => {
@@ -127,8 +131,7 @@ describe("guardian control-plane proxy", () => {
127
131
 
128
132
  expect(res.status).toBe(200);
129
133
  expect(capturedBody).toBe('{"channel":"voice","destination":"+15551234567"}');
130
- expect(capturedHeaders?.get("authorization")).toBe("Bearer runtime-token");
131
- expect(capturedHeaders?.get("X-Gateway-Origin")).toBe("gateway-origin");
134
+ expect(capturedHeaders?.get("authorization")).toMatch(/^Bearer ey/);
132
135
  expect(capturedHeaders?.has("host")).toBe(false);
133
136
  });
134
137
 
@@ -1,5 +1,9 @@
1
1
  import { describe, test, expect, mock, afterEach } from "bun:test";
2
2
  import type { GatewayConfig } from "../config.js";
3
+ import { initSigningKey } from "../auth/token-service.js";
4
+
5
+ const TEST_SIGNING_KEY = Buffer.from('test-signing-key-at-least-32-bytes-long');
6
+ initSigningKey(TEST_SIGNING_KEY);
3
7
 
4
8
  type FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
5
9
  let fetchMock: ReturnType<typeof mock<FetchFn>> = mock(async () => new Response());
@@ -116,7 +120,7 @@ describe("ingress control-plane proxy", () => {
116
120
  ]);
117
121
  });
118
122
 
119
- test("replaces caller auth with runtime auth and forwards gateway-origin proof", async () => {
123
+ test("replaces caller auth with runtime auth", async () => {
120
124
  let capturedHeaders: Headers | undefined;
121
125
  fetchMock = mock(async (_input: string | URL | Request, init?: RequestInit) => {
122
126
  capturedHeaders = init?.headers as unknown as Headers;
@@ -136,8 +140,7 @@ describe("ingress control-plane proxy", () => {
136
140
  );
137
141
 
138
142
  expect(res.status).toBe(200);
139
- expect(capturedHeaders?.get("authorization")).toBe("Bearer runtime-token");
140
- expect(capturedHeaders?.get("X-Gateway-Origin")).toBe("gateway-origin");
143
+ expect(capturedHeaders?.get("authorization")).toMatch(/^Bearer ey/);
141
144
  expect(capturedHeaders?.has("host")).toBe(false);
142
145
  });
143
146
 
@@ -1,5 +1,9 @@
1
1
  import { describe, test, expect, mock, afterEach } from "bun:test";
2
2
  import type { GatewayConfig } from "../config.js";
3
+ import { initSigningKey } from "../auth/token-service.js";
4
+
5
+ const TEST_SIGNING_KEY = Buffer.from('test-signing-key-at-least-32-bytes-long');
6
+ initSigningKey(TEST_SIGNING_KEY);
3
7
 
4
8
  type FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
5
9
  let fetchMock: ReturnType<typeof mock<FetchFn>> = mock(async () => new Response());
@@ -96,7 +100,7 @@ describe("OAuth callback handler", () => {
96
100
  expect(sentBody.error).toBeUndefined();
97
101
 
98
102
  const headers = calledInit.headers as Record<string, string>;
99
- expect(headers["Authorization"]).toBe("Bearer rt-token");
103
+ expect(headers["Authorization"]).toMatch(/^Bearer ey/);
100
104
  });
101
105
 
102
106
  test("missing state parameter returns 400 error page", async () => {
@@ -317,7 +321,7 @@ describe("OAuth callback handler", () => {
317
321
  expect(fetchMock.mock.calls.length).toBe(MAX_CONSUMED_STATES + 1);
318
322
  });
319
323
 
320
- test("omits Authorization header when runtimeBearerToken is undefined", async () => {
324
+ test("always sends JWT Authorization header to runtime", async () => {
321
325
  fetchMock = mock(async () =>
322
326
  Response.json({ ok: true }, { status: 200 }),
323
327
  );
@@ -334,6 +338,6 @@ describe("OAuth callback handler", () => {
334
338
 
335
339
  const calledInit = (fetchMock.mock.calls[0] as unknown[])[1] as RequestInit;
336
340
  const headers = calledInit.headers as Record<string, string>;
337
- expect(headers["Authorization"]).toBeUndefined();
341
+ expect(headers["Authorization"]).toMatch(/^Bearer ey/);
338
342
  });
339
343
  });
@@ -1,6 +1,10 @@
1
1
  import { describe, test, expect, mock, afterEach } from "bun:test";
2
2
  import type { RuntimeAttachmentMeta, RuntimeInboundPayload } from "../runtime/client.js";
3
3
  import type { GatewayConfig } from "../config.js";
4
+ import { initSigningKey } from "../auth/token-service.js";
5
+
6
+ const TEST_SIGNING_KEY = Buffer.from('test-signing-key-at-least-32-bytes-long');
7
+ initSigningKey(TEST_SIGNING_KEY);
4
8
 
5
9
  type FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
6
10
  let fetchMock: ReturnType<typeof mock<FetchFn>> = mock(async () => new Response());
@@ -182,7 +186,7 @@ describe("forwardToRuntime", () => {
182
186
  expect(attachments[0].kind).toBe("generated_image");
183
187
  });
184
188
 
185
- test("sends Authorization header when runtimeBearerToken is configured", async () => {
189
+ test("sends JWT Authorization header to runtime", async () => {
186
190
  fetchMock = mock(async () =>
187
191
  new Response(JSON.stringify(successBody), { status: 200 }),
188
192
  );
@@ -192,20 +196,7 @@ describe("forwardToRuntime", () => {
192
196
 
193
197
  const calledInit = (fetchMock.mock.calls[0] as unknown[])[1] as RequestInit;
194
198
  const headers = calledInit.headers as Record<string, string>;
195
- expect(headers["Authorization"]).toBe("Bearer my-secret-token");
196
- });
197
-
198
- test("omits Authorization header when runtimeBearerToken is undefined", async () => {
199
- fetchMock = mock(async () =>
200
- new Response(JSON.stringify(successBody), { status: 200 }),
201
- );
202
-
203
- const config = makeConfig();
204
- await forwardToRuntime(config, payload);
205
-
206
- const calledInit = (fetchMock.mock.calls[0] as unknown[])[1] as RequestInit;
207
- const headers = calledInit.headers as Record<string, string>;
208
- expect(headers["Authorization"]).toBeUndefined();
199
+ expect(headers["Authorization"]).toMatch(/^Bearer ey/);
209
200
  });
210
201
  });
211
202
 
@@ -282,7 +273,7 @@ describe("forwardTwilioVoiceWebhook", () => {
282
273
  expect(sentBody.originalUrl).toBe(originalUrl);
283
274
 
284
275
  const headers = calledInit.headers as Record<string, string>;
285
- expect(headers["Authorization"]).toBe("Bearer rt-tok");
276
+ expect(headers["Authorization"]).toMatch(/^Bearer ey/);
286
277
  });
287
278
  });
288
279
 
@@ -1,5 +1,9 @@
1
1
  import { describe, test, expect, mock, afterEach } from "bun:test";
2
2
  import type { GatewayConfig } from "../config.js";
3
+ import { initSigningKey } from "../auth/token-service.js";
4
+
5
+ const TEST_SIGNING_KEY = Buffer.from('test-signing-key-at-least-32-bytes-long');
6
+ initSigningKey(TEST_SIGNING_KEY);
3
7
 
4
8
  type FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
5
9
  let fetchMock: ReturnType<typeof mock<FetchFn>> = mock(async () => new Response());
@@ -84,7 +88,7 @@ describe("runtime health proxy", () => {
84
88
  expect(await res.json()).toEqual({ status: "healthy" });
85
89
  });
86
90
 
87
- test("replaces caller auth with runtime auth and forwards gateway-origin proof", async () => {
91
+ test("replaces caller auth with runtime auth", async () => {
88
92
  let capturedHeaders: Headers | undefined;
89
93
  fetchMock = mock(async (_input: string | URL | Request, init?: RequestInit) => {
90
94
  capturedHeaders = init?.headers as unknown as Headers;
@@ -102,8 +106,7 @@ describe("runtime health proxy", () => {
102
106
  );
103
107
 
104
108
  expect(res.status).toBe(200);
105
- expect(capturedHeaders?.get("authorization")).toBe("Bearer runtime-token");
106
- expect(capturedHeaders?.get("X-Gateway-Origin")).toBe("gateway-origin");
109
+ expect(capturedHeaders?.get("authorization")).toMatch(/^Bearer ey/);
107
110
  expect(capturedHeaders?.has("host")).toBe(false);
108
111
  });
109
112
 
@@ -1,4 +1,4 @@
1
- import { describe, test, expect, mock, afterEach, beforeAll } from "bun:test";
1
+ import { describe, test, expect, mock, afterEach } from "bun:test";
2
2
  import type { GatewayConfig } from "../config.js";
3
3
  import { initSigningKey, mintToken } from "../auth/token-service.js";
4
4
  import { CURRENT_POLICY_EPOCH } from "../auth/policy.js";
@@ -1,5 +1,9 @@
1
1
  import { describe, test, expect, mock, afterEach } from "bun:test";
2
2
  import type { GatewayConfig } from "../config.js";
3
+ import { initSigningKey } from "../auth/token-service.js";
4
+
5
+ const TEST_SIGNING_KEY = Buffer.from('test-signing-key-at-least-32-bytes-long');
6
+ initSigningKey(TEST_SIGNING_KEY);
3
7
 
4
8
  type FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
5
9
  let fetchMock: ReturnType<typeof mock<FetchFn>> = mock(async () => new Response());
@@ -223,7 +227,7 @@ describe("runtime proxy handler", () => {
223
227
  expect(capturedSignal).toBeInstanceOf(AbortSignal);
224
228
  });
225
229
 
226
- test("forwards authorization header when auth is not required", async () => {
230
+ test("replaces client authorization with JWT service token when auth is not required", async () => {
227
231
  let capturedHeaders: Headers | undefined;
228
232
  fetchMock = mock(async (_input: string | URL | Request, init?: RequestInit) => {
229
233
  capturedHeaders = init?.headers as unknown as Headers;
@@ -236,10 +240,11 @@ describe("runtime proxy handler", () => {
236
240
  });
237
241
  await handler(req);
238
242
 
239
- expect(capturedHeaders!.get("authorization")).toBe("Bearer upstream-token");
243
+ // When auth is not required, gateway still mints a JWT service token for the runtime
244
+ expect(capturedHeaders!.get("authorization")).toMatch(/^Bearer ey/);
240
245
  });
241
246
 
242
- test("replaces client authorization with configured bearer token for upstream", async () => {
247
+ test("replaces client authorization with JWT token for upstream", async () => {
243
248
  let capturedHeaders: Headers | undefined;
244
249
  fetchMock = mock(async (_input: string | URL | Request, init?: RequestInit) => {
245
250
  capturedHeaders = init?.headers as unknown as Headers;
@@ -254,7 +259,7 @@ describe("runtime proxy handler", () => {
254
259
  });
255
260
  await handler(req);
256
261
 
257
- expect(capturedHeaders!.get("authorization")).toBe("Bearer daemon-token");
262
+ expect(capturedHeaders!.get("authorization")).toMatch(/^Bearer ey/);
258
263
  });
259
264
 
260
265
  test("truncates long upstream error bodies in logs", async () => {
@@ -397,48 +402,4 @@ describe("runtime proxy handler", () => {
397
402
  expect(fetchCalls.length).toBe(1);
398
403
  });
399
404
  });
400
-
401
- // ── Gateway-origin header on proxied requests ────────────────────────
402
-
403
- describe("gateway-origin header", () => {
404
- test("sets X-Gateway-Origin header when runtimeBearerToken is configured", async () => {
405
- let capturedHeaders: Headers | undefined;
406
- fetchMock = mock(async (_input: string | URL | Request, init?: RequestInit) => {
407
- capturedHeaders = init?.headers as unknown as Headers;
408
- return new Response("ok", { status: 200 });
409
- });
410
-
411
- const handler = createRuntimeProxyHandler(
412
- makeConfig({}),
413
- );
414
- const req = new Request("http://localhost:7830/v1/channels/inbound", {
415
- method: "POST",
416
- headers: { "content-type": "application/json" },
417
- body: JSON.stringify({ message: "hello" }),
418
- });
419
- await handler(req);
420
-
421
- expect(capturedHeaders!.get("x-gateway-origin")).toBe("runtime-secret");
422
- });
423
-
424
- test("does not set X-Gateway-Origin header when no runtimeBearerToken", async () => {
425
- let capturedHeaders: Headers | undefined;
426
- fetchMock = mock(async (_input: string | URL | Request, init?: RequestInit) => {
427
- capturedHeaders = init?.headers as unknown as Headers;
428
- return new Response("ok", { status: 200 });
429
- });
430
-
431
- const handler = createRuntimeProxyHandler(
432
- makeConfig({}),
433
- );
434
- const req = new Request("http://localhost:7830/v1/channels/inbound", {
435
- method: "POST",
436
- headers: { "content-type": "application/json" },
437
- body: JSON.stringify({ message: "hello" }),
438
- });
439
- await handler(req);
440
-
441
- expect(capturedHeaders!.has("x-gateway-origin")).toBe(false);
442
- });
443
- });
444
405
  });
@@ -57,8 +57,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
57
57
  return merged;
58
58
  }
59
59
 
60
- const TOKEN = "test-deliver-token";
61
-
62
60
  function makeRequest(
63
61
  body: unknown,
64
62
  headers?: Record<string, string>,
@@ -1,5 +1,9 @@
1
1
  import { describe, test, expect, mock, afterEach } from "bun:test";
2
2
  import type { GatewayConfig } from "../config.js";
3
+ import { initSigningKey } from "../auth/token-service.js";
4
+
5
+ const TEST_SIGNING_KEY = Buffer.from('test-signing-key-at-least-32-bytes-long');
6
+ initSigningKey(TEST_SIGNING_KEY);
3
7
 
4
8
  type FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
5
9
  let fetchMock: ReturnType<typeof mock<FetchFn>> = mock(async () => new Response());
@@ -101,7 +105,7 @@ describe("telegram control-plane proxy", () => {
101
105
  ]);
102
106
  });
103
107
 
104
- test("replaces caller auth with runtime auth and forwards gateway-origin proof", async () => {
108
+ test("replaces caller auth with runtime auth", async () => {
105
109
  let capturedHeaders: Headers | undefined;
106
110
  fetchMock = mock(async (_input: string | URL | Request, init?: RequestInit) => {
107
111
  capturedHeaders = init?.headers as unknown as Headers;
@@ -122,8 +126,7 @@ describe("telegram control-plane proxy", () => {
122
126
  );
123
127
 
124
128
  expect(res.status).toBe(200);
125
- expect(capturedHeaders?.get("authorization")).toBe("Bearer runtime-token");
126
- expect(capturedHeaders?.get("X-Gateway-Origin")).toBe("gateway-origin");
129
+ expect(capturedHeaders?.get("authorization")).toMatch(/^Bearer ey/);
127
130
  expect(capturedHeaders?.has("host")).toBe(false);
128
131
  });
129
132
 
@@ -1,5 +1,10 @@
1
1
  import { describe, test, expect, mock, afterEach } from "bun:test";
2
2
  import type { GatewayConfig } from "../config.js";
3
+ import { initSigningKey, mintToken } from "../auth/token-service.js";
4
+ import { CURRENT_POLICY_EPOCH } from "../auth/policy.js";
5
+
6
+ const TEST_SIGNING_KEY = Buffer.from('test-signing-key-at-least-32-bytes-long');
7
+ initSigningKey(TEST_SIGNING_KEY);
3
8
 
4
9
  type FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
5
10
  let fetchMock: ReturnType<typeof mock<FetchFn>> = mock(async () => new Response());
@@ -10,7 +15,18 @@ mock.module("../fetch.js", () => ({
10
15
 
11
16
  const { createTelegramDeliverHandler } = await import("../http/routes/telegram-deliver.js");
12
17
 
13
- const TOKEN = "test-deliver-token";
18
+ /** Mint a valid daemon JWT for deliver auth. */
19
+ function mintDeliverToken(): string {
20
+ return mintToken({
21
+ aud: 'vellum-daemon',
22
+ sub: 'svc:gateway:self',
23
+ scope_profile: 'gateway_service_v1',
24
+ policy_epoch: CURRENT_POLICY_EPOCH,
25
+ ttlSeconds: 300,
26
+ });
27
+ }
28
+
29
+ const TOKEN = mintDeliverToken();
14
30
 
15
31
  function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
16
32
  const merged: GatewayConfig = {
@@ -354,22 +370,6 @@ describe("/deliver/telegram bearer auth enforcement", () => {
354
370
  expect(body.ok).toBe(true);
355
371
  });
356
372
 
357
- test("returns 503 when no token is configured and bypass is not set", async () => {
358
- const handler = createTelegramDeliverHandler(
359
- makeConfig({}),
360
- );
361
- const req = new Request("http://localhost:7830/deliver/telegram", {
362
- method: "POST",
363
- headers: { "content-type": "application/json" },
364
- body: JSON.stringify({ chatId: "123", text: "hello" }),
365
- });
366
- const res = await handler(req);
367
-
368
- expect(res.status).toBe(503);
369
- const body = await res.json();
370
- expect(body.error).toBe("Service not configured: bearer token required");
371
- });
372
-
373
373
  test("allows unauthenticated access when bypass flag is set and no token configured", async () => {
374
374
  mockTelegramApi();
375
375
  const handler = createTelegramDeliverHandler(
@@ -387,23 +387,6 @@ describe("/deliver/telegram bearer auth enforcement", () => {
387
387
  expect(body.ok).toBe(true);
388
388
  });
389
389
 
390
- test("bypass flag is ignored when a bearer token is configured (auth still required)", async () => {
391
- const handler = createTelegramDeliverHandler(
392
- makeConfig({ telegramDeliverAuthBypass: true }),
393
- );
394
- const req = new Request("http://localhost:7830/deliver/telegram", {
395
- method: "POST",
396
- headers: { "content-type": "application/json" },
397
- body: JSON.stringify({ chatId: "123", text: "hello" }),
398
- });
399
- const res = await handler(req);
400
-
401
- // Token is configured, so missing Authorization header is still rejected
402
- expect(res.status).toBe(401);
403
- const body = await res.json();
404
- expect(body.error).toBe("Unauthorized");
405
- });
406
-
407
390
  test("still rejects non-POST methods before auth check", async () => {
408
391
  const handler = createTelegramDeliverHandler(makeConfig());
409
392
  const req = new Request("http://localhost:7830/deliver/telegram", {
@@ -1,5 +1,23 @@
1
1
  import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test";
2
2
  import type { GatewayConfig } from "../config.js";
3
+ import { initSigningKey, mintToken } from "../auth/token-service.js";
4
+ import { CURRENT_POLICY_EPOCH } from "../auth/policy.js";
5
+
6
+ const TEST_SIGNING_KEY = Buffer.from('test-signing-key-at-least-32-bytes-long');
7
+ initSigningKey(TEST_SIGNING_KEY);
8
+
9
+ /** Mint a valid daemon JWT for reconcile auth. */
10
+ function mintDaemonToken(): string {
11
+ return mintToken({
12
+ aud: 'vellum-daemon',
13
+ sub: 'svc:gateway:self',
14
+ scope_profile: 'gateway_service_v1',
15
+ policy_epoch: CURRENT_POLICY_EPOCH,
16
+ ttlSeconds: 300,
17
+ });
18
+ }
19
+
20
+ const TOKEN = mintDaemonToken();
3
21
 
4
22
  type FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
5
23
  let fetchMock: ReturnType<typeof mock<FetchFn>> = mock(async () => new Response());
@@ -125,15 +143,6 @@ describe("POST /internal/telegram/reconcile", () => {
125
143
  expect(res.status).toBe(405);
126
144
  });
127
145
 
128
- test("returns 503 when no bearer token is configured", async () => {
129
- const config = makeConfig({});
130
- const handler = createTelegramReconcileHandler(config);
131
- const res = await handler(makeRequest("POST", "any-token"));
132
- expect(res.status).toBe(503);
133
- const body = await res.json();
134
- expect(body.error).toContain("bearer token required");
135
- });
136
-
137
146
  test("returns 401 for missing auth header", async () => {
138
147
  const config = makeConfig();
139
148
  const handler = createTelegramReconcileHandler(config);
@@ -151,7 +160,7 @@ describe("POST /internal/telegram/reconcile", () => {
151
160
  test("triggers reconcile with correct auth", async () => {
152
161
  const config = makeConfig();
153
162
  const handler = createTelegramReconcileHandler(config);
154
- const res = await handler(makeRequest("POST", "test-token"));
163
+ const res = await handler(makeRequest("POST", TOKEN));
155
164
  expect(res.status).toBe(200);
156
165
  const body = await res.json();
157
166
  expect(body.ok).toBe(true);
@@ -163,7 +172,7 @@ describe("POST /internal/telegram/reconcile", () => {
163
172
  const config = makeConfig({ ingressPublicBaseUrl: "https://old.example.com" });
164
173
  const handler = createTelegramReconcileHandler(config);
165
174
  const res = await handler(
166
- makeRequest("POST", "test-token", {
175
+ makeRequest("POST", TOKEN, {
167
176
  ingressPublicBaseUrl: "https://new.example.com/",
168
177
  }),
169
178
  );
@@ -176,7 +185,7 @@ describe("POST /internal/telegram/reconcile", () => {
176
185
  const config = makeConfig({ ingressPublicBaseUrl: "https://old.example.com" });
177
186
  const handler = createTelegramReconcileHandler(config);
178
187
  const res = await handler(
179
- makeRequest("POST", "test-token", {
188
+ makeRequest("POST", TOKEN, {
180
189
  ingressPublicBaseUrl: "",
181
190
  }),
182
191
  );
@@ -190,7 +199,7 @@ describe("POST /internal/telegram/reconcile", () => {
190
199
  const req = new Request("http://localhost:7830/internal/telegram/reconcile", {
191
200
  method: "POST",
192
201
  headers: {
193
- authorization: "Bearer test-token",
202
+ authorization: `Bearer ${TOKEN}`,
194
203
  },
195
204
  });
196
205
  const res = await handler(req);
@@ -204,7 +213,7 @@ describe("POST /internal/telegram/reconcile", () => {
204
213
  });
205
214
  const config = makeConfig();
206
215
  const handler = createTelegramReconcileHandler(config);
207
- const res = await handler(makeRequest("POST", "test-token"));
216
+ const res = await handler(makeRequest("POST", TOKEN));
208
217
  expect(res.status).toBe(502);
209
218
  const body = await res.json();
210
219
  expect(body.error).toBe("Reconciliation failed");
@@ -216,7 +225,7 @@ describe("POST /internal/telegram/reconcile", () => {
216
225
  const req = new Request("http://localhost:7830/internal/telegram/reconcile", {
217
226
  method: "POST",
218
227
  headers: {
219
- authorization: "Bearer test-token",
228
+ authorization: `Bearer ${TOKEN}`,
220
229
  "content-type": "application/json",
221
230
  },
222
231
  body: "not-json{",
@@ -263,12 +272,12 @@ describe("POST /internal/telegram/reconcile", () => {
263
272
  // is still running, leaving Telegram pointed at the first URL.
264
273
  const [res1, res2] = await Promise.all([
265
274
  handler(
266
- makeRequest("POST", "test-token", {
275
+ makeRequest("POST", TOKEN, {
267
276
  ingressPublicBaseUrl: "https://first.com",
268
277
  }),
269
278
  ),
270
279
  handler(
271
- makeRequest("POST", "test-token", {
280
+ makeRequest("POST", TOKEN, {
272
281
  ingressPublicBaseUrl: "https://second.com",
273
282
  }),
274
283
  ),
@@ -1,6 +1,10 @@
1
1
  import { describe, test, expect, mock, afterEach } from "bun:test";
2
2
  import type { RuntimeAttachmentMeta } from "../runtime/client.js";
3
3
  import type { GatewayConfig } from "../config.js";
4
+ import { initSigningKey } from "../auth/token-service.js";
5
+
6
+ const TEST_SIGNING_KEY = Buffer.from('test-signing-key-at-least-32-bytes-long');
7
+ initSigningKey(TEST_SIGNING_KEY);
4
8
 
5
9
  type FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
6
10
  let fetchMock: ReturnType<typeof mock<FetchFn>> = mock(async () => new Response());
@@ -1,5 +1,9 @@
1
1
  import { describe, test, expect, mock, afterEach, beforeEach } from "bun:test";
2
2
  import type { GatewayConfig } from "../config.js";
3
+ import { initSigningKey } from "../auth/token-service.js";
4
+
5
+ const TEST_SIGNING_KEY = Buffer.from('test-signing-key-at-least-32-bytes-long');
6
+ initSigningKey(TEST_SIGNING_KEY);
3
7
 
4
8
  type FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
5
9
  let fetchMock: ReturnType<typeof mock<FetchFn>> = mock(async () => new Response());
@@ -557,41 +561,3 @@ describe("telegram webhook handler: callback_query forwarding", () => {
557
561
  });
558
562
  });
559
563
 
560
- describe("telegram webhook handler: gateway-origin marker", () => {
561
- test("forwards X-Gateway-Origin header when runtimeBearerToken is configured", async () => {
562
- const bearerToken = "secret-runtime-token";
563
- const config = makeConfig({
564
- routingEntries: [{ type: "conversation_id", key: "12345", assistantId: "assistant-a" }],
565
- });
566
- installFetchMock();
567
- const { handler } = createTelegramWebhookHandler(config);
568
-
569
- const payload = makeTelegramPayload("hello", 8001);
570
- const req = makeWebhookRequest(payload);
571
- const res = await handler(req);
572
-
573
- expect(res.status).toBe(200);
574
-
575
- const runtimeCall = fetchCalls.find((c) => c.url.includes("/inbound"));
576
- expect(runtimeCall).toBeDefined();
577
- expect(runtimeCall!.headers?.["x-gateway-origin"]).toBe(bearerToken);
578
- });
579
-
580
- test("does not include X-Gateway-Origin header when no runtimeBearerToken", async () => {
581
- const config = makeConfig({
582
- routingEntries: [{ type: "conversation_id", key: "12345", assistantId: "assistant-a" }],
583
- });
584
- installFetchMock();
585
- const { handler } = createTelegramWebhookHandler(config);
586
-
587
- const payload = makeTelegramPayload("hello", 8002);
588
- const req = makeWebhookRequest(payload);
589
- const res = await handler(req);
590
-
591
- expect(res.status).toBe(200);
592
-
593
- const runtimeCall = fetchCalls.find((c) => c.url.includes("/inbound"));
594
- expect(runtimeCall).toBeDefined();
595
- expect(runtimeCall!.headers?.["x-gateway-origin"]).toBeUndefined();
596
- });
597
- });
@@ -1,6 +1,10 @@
1
1
  import { describe, test, expect, mock, afterEach } from "bun:test";
2
2
  import { createHmac } from "node:crypto";
3
3
  import type { GatewayConfig } from "../config.js";
4
+ import { initSigningKey } from "../auth/token-service.js";
5
+
6
+ const TEST_SIGNING_KEY = Buffer.from('test-signing-key-at-least-32-bytes-long');
7
+ initSigningKey(TEST_SIGNING_KEY);
4
8
 
5
9
  type FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
6
10
  let fetchMock: ReturnType<typeof mock<FetchFn>> = mock(async () => new Response());
@@ -42,6 +42,9 @@ const PROFILE_SCOPES: Record<ScopeProfile, ReadonlySet<Scope>> = {
42
42
  ipc_v1: new Set<Scope>([
43
43
  'ipc.all',
44
44
  ]),
45
+ ui_page_v1: new Set<Scope>([
46
+ 'settings.read',
47
+ ]),
45
48
  };
46
49
 
47
50
  // ---------------------------------------------------------------------------
package/src/auth/types.ts CHANGED
@@ -13,7 +13,8 @@ export type ScopeProfile =
13
13
  | 'actor_client_v1'
14
14
  | 'gateway_ingress_v1'
15
15
  | 'gateway_service_v1'
16
- | 'ipc_v1';
16
+ | 'ipc_v1'
17
+ | 'ui_page_v1';
17
18
 
18
19
  // ---------------------------------------------------------------------------
19
20
  // Individual scope strings
@@ -1,5 +1,10 @@
1
1
  import { describe, it, expect, mock, afterEach } from "bun:test";
2
2
  import type { GatewayConfig } from "../../config.js";
3
+ import { initSigningKey, mintToken } from "../../auth/token-service.js";
4
+ import { CURRENT_POLICY_EPOCH } from "../../auth/policy.js";
5
+
6
+ const TEST_SIGNING_KEY = Buffer.from('test-signing-key-at-least-32-bytes-long');
7
+ initSigningKey(TEST_SIGNING_KEY);
3
8
 
4
9
  type FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
5
10
  let fetchMock: ReturnType<typeof mock<FetchFn>> = mock(async () => new Response());
@@ -12,7 +17,18 @@ const { createSmsDeliverHandler } = await import("./sms-deliver.js");
12
17
 
13
18
  // --- Helpers ---------------------------------------------------------------
14
19
 
15
- const TOKEN = "test-deliver-token";
20
+ /** Mint a valid daemon JWT for deliver auth. */
21
+ function mintDeliverToken(): string {
22
+ return mintToken({
23
+ aud: 'vellum-daemon',
24
+ sub: 'svc:gateway:self',
25
+ scope_profile: 'gateway_service_v1',
26
+ policy_epoch: CURRENT_POLICY_EPOCH,
27
+ ttlSeconds: 300,
28
+ });
29
+ }
30
+
31
+ const TOKEN = mintDeliverToken();
16
32
 
17
33
  function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
18
34
  const merged: GatewayConfig = {
@@ -97,17 +113,6 @@ describe("/deliver/sms", () => {
97
113
  expect(res.status).toBe(405);
98
114
  });
99
115
 
100
- it("rejects when no bearer token and bypass not set with 503", async () => {
101
- const handler = createSmsDeliverHandler(
102
- makeConfig({}),
103
- );
104
- const req = makeRequest({ to: "+15559876543", text: "hello" });
105
- const res = await handler(req);
106
- expect(res.status).toBe(503);
107
- const body = await res.json();
108
- expect(body.error).toBe("Service not configured: bearer token required");
109
- });
110
-
111
116
  it("rejects request without Authorization header with 401", async () => {
112
117
  const handler = createSmsDeliverHandler(makeConfig());
113
118
  const req = makeRequest({ to: "+15559876543", text: "hello" });
@@ -1,7 +1,12 @@
1
1
  import { describe, it, expect, mock, beforeEach } from "bun:test";
2
2
  import type { GatewayConfig } from "../../config.js";
3
+ import { initSigningKey, mintToken } from "../../auth/token-service.js";
4
+ import { CURRENT_POLICY_EPOCH } from "../../auth/policy.js";
3
5
  import { createTelegramDeliverHandler } from "./telegram-deliver.js";
4
6
 
7
+ const TEST_SIGNING_KEY = Buffer.from('test-signing-key-at-least-32-bytes-long');
8
+ initSigningKey(TEST_SIGNING_KEY);
9
+
5
10
  // ---- Mocks ----
6
11
 
7
12
  // Track calls to sendTelegramReply so we can assert on approval passthrough.
@@ -68,7 +73,18 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
68
73
  return merged;
69
74
  }
70
75
 
71
- const TOKEN = "test-deliver-token";
76
+ /** Mint a valid daemon JWT for deliver auth. */
77
+ function mintDeliverToken(): string {
78
+ return mintToken({
79
+ aud: 'vellum-daemon',
80
+ sub: 'svc:gateway:self',
81
+ scope_profile: 'gateway_service_v1',
82
+ policy_epoch: CURRENT_POLICY_EPOCH,
83
+ ttlSeconds: 300,
84
+ });
85
+ }
86
+
87
+ const TOKEN = mintDeliverToken();
72
88
 
73
89
  function makeRequest(body: unknown, headers?: Record<string, string>): Request {
74
90
  return new Request("http://localhost:7830/deliver/telegram", {
@@ -97,17 +113,6 @@ describe("telegram-deliver endpoint basics", () => {
97
113
  expect(body.error).toBe("Method not allowed");
98
114
  });
99
115
 
100
- it("rejects when no bearer token and bypass not set with 503", async () => {
101
- const handler = createTelegramDeliverHandler(
102
- makeConfig({ telegramDeliverAuthBypass: false }),
103
- );
104
- const req = makeRequest({ chatId: "123", text: "hello" });
105
- const res = await handler(req);
106
- expect(res.status).toBe(503);
107
- const body = await res.json();
108
- expect(body.error).toBe("Service not configured: bearer token required");
109
- });
110
-
111
116
  it("rejects request without Authorization header with 401", async () => {
112
117
  const handler = createTelegramDeliverHandler(
113
118
  makeConfig({ telegramDeliverAuthBypass: false }),
@@ -1,5 +1,10 @@
1
1
  import { describe, it, expect, mock, beforeEach } from "bun:test";
2
2
  import type { GatewayConfig } from "../../config.js";
3
+ import { initSigningKey, mintToken } from "../../auth/token-service.js";
4
+ import { CURRENT_POLICY_EPOCH } from "../../auth/policy.js";
5
+
6
+ const TEST_SIGNING_KEY = Buffer.from('test-signing-key-at-least-32-bytes-long');
7
+ initSigningKey(TEST_SIGNING_KEY);
3
8
 
4
9
  // ---- Mocks ----
5
10
 
@@ -16,7 +21,18 @@ const { createWhatsAppDeliverHandler } = await import("./whatsapp-deliver.js");
16
21
 
17
22
  // ---- Helpers ----
18
23
 
19
- const TOKEN = "test-deliver-token";
24
+ /** Mint a valid daemon JWT for deliver auth. */
25
+ function mintDeliverToken(): string {
26
+ return mintToken({
27
+ aud: 'vellum-daemon',
28
+ sub: 'svc:gateway:self',
29
+ scope_profile: 'gateway_service_v1',
30
+ policy_epoch: CURRENT_POLICY_EPOCH,
31
+ ttlSeconds: 300,
32
+ });
33
+ }
34
+
35
+ const TOKEN = mintDeliverToken();
20
36
 
21
37
  function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
22
38
  const merged: GatewayConfig = {
@@ -94,22 +110,9 @@ describe("/deliver/whatsapp", () => {
94
110
  expect(body.error).toBe("Method not allowed");
95
111
  });
96
112
 
97
- it("rejects when no bearer token and bypass not set with 503", async () => {
98
- const handler = createWhatsAppDeliverHandler(
99
- makeConfig({
100
- whatsappDeliverAuthBypass: false,
101
- }),
102
- );
103
- const req = makeRequest({ to: "+15559876543", text: "hello" });
104
- const res = await handler(req);
105
- expect(res.status).toBe(503);
106
- const body = await res.json();
107
- expect(body.error).toBe("Service not configured: bearer token required");
108
- });
109
-
110
113
  it("rejects request without Authorization header with 401", async () => {
111
114
  const handler = createWhatsAppDeliverHandler(
112
- makeConfig({}),
115
+ makeConfig({ whatsappDeliverAuthBypass: false }),
113
116
  );
114
117
  const req = makeRequest({ to: "+15559876543", text: "hello" });
115
118
  const res = await handler(req);
@@ -120,7 +123,7 @@ describe("/deliver/whatsapp", () => {
120
123
 
121
124
  it("rejects request with wrong bearer token with 401", async () => {
122
125
  const handler = createWhatsAppDeliverHandler(
123
- makeConfig({}),
126
+ makeConfig({ whatsappDeliverAuthBypass: false }),
124
127
  );
125
128
  const req = makeRequest({ to: "+15559876543", text: "hello" }, {
126
129
  authorization: "Bearer wrong-token",
@@ -133,7 +136,7 @@ describe("/deliver/whatsapp", () => {
133
136
 
134
137
  it("accepts request with correct bearer token", async () => {
135
138
  const handler = createWhatsAppDeliverHandler(
136
- makeConfig({}),
139
+ makeConfig({ whatsappDeliverAuthBypass: false }),
137
140
  );
138
141
  const req = makeRequest({ to: "+15559876543", text: "hello" }, {
139
142
  authorization: `Bearer ${TOKEN}`,
package/src/index.ts CHANGED
@@ -2,12 +2,12 @@ process.title = "vellum-gateway";
2
2
 
3
3
  import { randomBytes } from "node:crypto";
4
4
  import { AuthRateLimiter } from "./auth-rate-limiter.js";
5
- import { loadOrCreateSigningKey, initSigningKey, verifyToken } from "./auth/token-service.js";
5
+ import { loadOrCreateSigningKey, initSigningKey } from "./auth/token-service.js";
6
6
  import { validateEdgeToken } from "./auth/token-exchange.js";
7
7
  import { resolveScopeProfile } from "./auth/scopes.js";
8
8
  import type { Scope } from "./auth/types.js";
9
9
  import { ConfigFileWatcher } from "./config-file-watcher.js";
10
- import { loadConfig, isSlackChannelConfigured, type GatewayConfig } from "./config.js";
10
+ import { loadConfig, isSlackChannelConfigured } from "./config.js";
11
11
  import { CredentialWatcher } from "./credential-watcher.js";
12
12
  import { createRuntimeProxyHandler } from "./http/routes/runtime-proxy.js";
13
13
  import {