diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f6524829391..a1eda818783 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -3432,6 +3432,43 @@ All public-facing URLs are constructed by `assistant/src/inbound/public-ingress- | `getOAuthCallbackUrl()` | `${base}/webhooks/oauth/callback` | | `getTelegramWebhookUrl()` | `${base}/webhooks/telegram` | +### Telegram Messaging Flow + +Telegram messages follow three paths through the system: + +``` +Inbound (user → assistant): + Telegram → Gateway POST /webhooks/telegram → verify secret → normalize → route + → Runtime POST /v1/assistants/:id/channels/inbound + +Outbound reply (assistant → user, triggered by inbound): + Runtime callback → Gateway POST /deliver/telegram (bearer auth) → Telegram sendMessage/sendPhoto/sendDocument + +Outbound proactive (assistant → user, initiated by messaging provider): + Runtime messaging provider → Gateway POST /deliver/telegram (bearer auth) → Telegram sendMessage +``` + +The `/deliver/telegram` endpoint requires bearer auth unconditionally (fail-closed). If no bearer token is configured and the dev-only bypass flag (`GATEWAY_TELEGRAM_DELIVER_AUTH_BYPASS`) is not set, the endpoint returns 503 rather than allowing unauthenticated access. + +### Webhook Reconciliation + +On startup, the gateway automatically reconciles the Telegram webhook registration: + +1. Reads `INGRESS_PUBLIC_BASE_URL` and Telegram credentials (bot token, webhook secret) +2. Calls `getWebhookInfo` to check the current registration +3. Compares the URL, secret, and allowed updates against the expected values +4. If any differ, calls `setWebhook` to update the registration + +This also runs when the credential watcher detects changes to Telegram credentials. Manual webhook registration is no longer required. + +### Routing Auto-Configuration + +In single-assistant mode (the default local deployment), routing is automatically configured by the CLI: +- `GATEWAY_UNMAPPED_POLICY=default` is set so all inbound messages are forwarded +- `GATEWAY_DEFAULT_ASSISTANT_ID` is set to the current assistant's ID + +In multi-assistant mode, the operator must configure `GATEWAY_ASSISTANT_ROUTING_JSON` to map specific chat/user IDs to assistant IDs. + --- ## Outgoing AI Phone Calls — Twilio ConversationRelay diff --git a/README.md b/README.md index 23b20f5c7c3..3258215bf52 100644 --- a/README.md +++ b/README.md @@ -203,11 +203,11 @@ If a proxied command receives a 401 or 403 despite having the correct credential Vellum integrates with third-party services via OAuth2. Each integration is exposed as a bundled skill with its own set of tools. -### Messaging (Gmail, Slack) +### Messaging (Gmail, Slack, Telegram) -The unified messaging layer provides platform-agnostic tools (`messaging_send`, `messaging_read`, `messaging_search`, etc.) that delegate to provider adapters. Gmail and Slack each implement the `MessagingProvider` interface. Platform-specific tools (e.g. `gmail_archive`, `slack_add_reaction`) extend beyond the generic interface where needed. +The unified messaging layer provides platform-agnostic tools (`messaging_send`, `messaging_read`, `messaging_search`, etc.) that delegate to provider adapters. Gmail and Slack each implement the `MessagingProvider` interface. Telegram is also supported as a messaging provider, though with limited capabilities compared to Slack and Gmail: bots can send messages to known chat IDs but cannot list conversations, retrieve message history, or search messages (Bot API limitations). Bots can only message users or groups that have previously interacted with the bot. Platform-specific tools (e.g. `gmail_archive`, `slack_add_reaction`) extend beyond the generic interface where needed. -Connect via the Settings UI or `integration_connect` IPC message. OAuth2 tokens are stored in the credential vault — the LLM never sees raw tokens. +Connect Gmail and Slack via the Settings UI or `integration_connect` IPC message. OAuth2 tokens are stored in the credential vault — the LLM never sees raw tokens. Telegram uses a bot token (not OAuth) — see the `telegram-setup` skill for setup instructions. ### Twitter (X) diff --git a/assistant/src/config/bundled-skills/messaging/SKILL.md b/assistant/src/config/bundled-skills/messaging/SKILL.md index b72475ad6f2..251abab6141 100644 --- a/assistant/src/config/bundled-skills/messaging/SKILL.md +++ b/assistant/src/config/bundled-skills/messaging/SKILL.md @@ -47,7 +47,7 @@ The telegram-setup skill handles: verifying the bot token from @BotFather, gener ## Capabilities -### Universal (all platforms) +### Universal (Slack, Gmail) - **Auth Test**: Verify connection and show account info - **List Conversations**: Show channels, inboxes, DMs with unread counts - **Read Messages**: Read message history from a conversation @@ -56,6 +56,21 @@ The telegram-setup skill handles: verifying the bot token from @BotFather, gener - **Reply**: Reply in a thread (medium risk) - **Mark Read**: Mark conversation as read +### Telegram +Telegram is supported as a messaging provider with limited capabilities compared to Slack and Gmail due to Bot API constraints: + +- **Send**: Send a message to a known chat ID (high risk — requires user approval) +- **Auth Test**: Verify bot token and show bot info + +**Not available** (Bot API limitations): +- List conversations — the Bot API does not expose a method to enumerate chats a bot belongs to +- Read message history — bots cannot retrieve past messages from a chat +- Search messages — no search API is available for bots + +**Bot-account limits:** +- The bot can only message users or groups that have previously interacted with it (sent `/start` or been added to a group). Bots cannot initiate conversations with arbitrary phone numbers. +- Future support for MTProto user-account sessions may lift some of these restrictions. + ### Slack-specific - **Add Reaction**: Add an emoji reaction to a message - **Leave Channel**: Leave a Slack channel diff --git a/assistant/src/config/vellum-skills/telegram-setup/SKILL.md b/assistant/src/config/vellum-skills/telegram-setup/SKILL.md index 64155fa2a84..24ca8344449 100644 --- a/assistant/src/config/vellum-skills/telegram-setup/SKILL.md +++ b/assistant/src/config/vellum-skills/telegram-setup/SKILL.md @@ -97,3 +97,33 @@ Summarize what was done: - Routing configuration validated The gateway automatically detects credentials from the vault, reconciles the Telegram webhook registration, and begins accepting Telegram webhooks shortly. In single-assistant mode, routing is automatically configured — no manual environment variable configuration or webhook registration is needed. If the ingress URL or secret changes later, the gateway will automatically re-register the webhook. + +## Bot-Account Limitations + +Telegram bot accounts have inherent limitations imposed by the Bot API: + +- **No arbitrary messaging**: Bots cannot initiate conversations with users who have not first interacted with the bot (sent `/start` or added it to a group). Messaging arbitrary phone numbers is not possible. +- **No conversation listing**: The Bot API does not expose a method to enumerate the chats a bot belongs to. +- **No message history retrieval**: Bots cannot fetch past messages from a chat. +- **No message search**: No search API is available for bots. + +These limitations apply to all Telegram bots regardless of configuration. Future support for MTProto user-account sessions may lift some of these restrictions. + +## Automated vs Manual Steps + +The following steps are now **automated** by the gateway and CLI: + +| Step | Status | Details | +|------|--------|---------| +| Webhook registration | Automated | The gateway reconciles the webhook URL on startup and when credentials change | +| Routing configuration | Automated (single-assistant) | The CLI sets `GATEWAY_UNMAPPED_POLICY=default` and `GATEWAY_DEFAULT_ASSISTANT_ID` automatically | +| Credential detection | Automated | The gateway watches the credential vault for changes | + +The following steps still require **manual** action: + +| Step | Details | +|------|---------| +| Bot token from @BotFather | User must create a bot and provide the token | +| Bot command registration | Registered via the setup skill (Step 4 above) | +| Credential storage | Stored via the setup skill (Step 5 above) | +| Multi-assistant routing | Requires manual `GATEWAY_ASSISTANT_ROUTING_JSON` configuration | diff --git a/gateway/README.md b/gateway/README.md index 66a327fdae4..52e4b056145 100644 --- a/gateway/README.md +++ b/gateway/README.md @@ -47,6 +47,7 @@ bun run dev | `GATEWAY_MAX_WEBHOOK_PAYLOAD_BYTES` | No | `1048576` | Max inbound webhook payload size (rejects with 413) | | `GATEWAY_MAX_ATTACHMENT_BYTES` | No | `20971520` | Max single attachment size (oversized are skipped) | | `GATEWAY_MAX_ATTACHMENT_CONCURRENCY` | No | `3` | Max concurrent attachment download/upload operations | +| `GATEWAY_TELEGRAM_DELIVER_AUTH_BYPASS` | No | `false` | Dev-only: skip bearer auth on `/deliver/telegram` when no token is configured | ## Routing @@ -67,13 +68,28 @@ v1 uses deterministic settings-based routing (no database): ## Setting up the Telegram webhook -After deploying the gateway, register the webhook with Telegram using the `setWebhook` API method. Pass: +Webhook registration is now handled automatically by the gateway. On startup, the gateway reconciles the Telegram webhook by comparing the current registration against `${INGRESS_PUBLIC_BASE_URL}/webhooks/telegram`. If the URL, secret, or allowed updates differ, the gateway re-registers the webhook automatically. This also runs whenever credentials change (e.g., tunnel restart, secret rotation). + +For manual setup (or reference), register the webhook with Telegram using the `setWebhook` API method. Pass: - `url` — your gateway URL, e.g. `https://your-host/webhooks/telegram` - The verify value matching your `TELEGRAM_WEBHOOK_SECRET` env var - `allowed_updates` — `["message", "edited_message"]` See the [Telegram Bot API docs](https://core.telegram.org/bots/api#setwebhook) for the full API reference. +## Telegram Deliver Endpoint Security + +The `/deliver/telegram` endpoint requires bearer auth by default (fail-closed). The security behavior is: + +| Condition | Result | +|-----------|--------| +| Bearer token configured + valid `Authorization` header | Request allowed | +| Bearer token configured + missing/invalid `Authorization` header | 401 Unauthorized | +| No bearer token configured + `GATEWAY_TELEGRAM_DELIVER_AUTH_BYPASS=true` | Request allowed (dev-only) | +| No bearer token configured + bypass not set | 503 Service Not Configured | + +This ensures that misconfiguration cannot expose an unauthenticated public message-send surface. In production, always configure `RUNTIME_PROXY_BEARER_TOKEN`. The `GATEWAY_TELEGRAM_DELIVER_AUTH_BYPASS` flag is intended for local development only. + ## Public Ingress Routes The gateway serves as the single public ingress point for all external callbacks. The following routes are handled directly by the gateway before any proxy forwarding: diff --git a/gateway/src/__tests__/config.test.ts b/gateway/src/__tests__/config.test.ts index fa219814ddc..3a7694692af 100644 --- a/gateway/src/__tests__/config.test.ts +++ b/gateway/src/__tests__/config.test.ts @@ -29,6 +29,7 @@ function withEnv(overrides: Record, fn: () => void) "GATEWAY_MAX_WEBHOOK_PAYLOAD_BYTES", "GATEWAY_MAX_ATTACHMENT_BYTES", "GATEWAY_MAX_ATTACHMENT_CONCURRENCY", + "GATEWAY_TELEGRAM_DELIVER_AUTH_BYPASS", "VELLUM_HTTP_TOKEN_PATH", ]; diff --git a/gateway/src/__tests__/load-guards.test.ts b/gateway/src/__tests__/load-guards.test.ts index 8d12e7d5df8..e4199b5d1ff 100644 --- a/gateway/src/__tests__/load-guards.test.ts +++ b/gateway/src/__tests__/load-guards.test.ts @@ -20,6 +20,7 @@ function makeConfig(overrides: Partial = {}): GatewayConfig { runtimeTimeoutMs: 30000, runtimeMaxRetries: 2, runtimeInitialBackoffMs: 500, + telegramDeliverAuthBypass: false, telegramInitialBackoffMs: 1000, telegramMaxRetries: 3, telegramTimeoutMs: 15000, diff --git a/gateway/src/__tests__/oauth-callback.test.ts b/gateway/src/__tests__/oauth-callback.test.ts index 67e208035c6..2c9c6e4498e 100644 --- a/gateway/src/__tests__/oauth-callback.test.ts +++ b/gateway/src/__tests__/oauth-callback.test.ts @@ -19,6 +19,7 @@ const makeConfig = (overrides: Partial = {}): GatewayConfig => ({ runtimeTimeoutMs: 30000, runtimeMaxRetries: 2, runtimeInitialBackoffMs: 500, + telegramDeliverAuthBypass: false, telegramInitialBackoffMs: 1000, telegramMaxRetries: 3, telegramTimeoutMs: 15000, diff --git a/gateway/src/__tests__/resolve-assistant.test.ts b/gateway/src/__tests__/resolve-assistant.test.ts index 85f7cca4e38..aa80809c6e2 100644 --- a/gateway/src/__tests__/resolve-assistant.test.ts +++ b/gateway/src/__tests__/resolve-assistant.test.ts @@ -20,6 +20,7 @@ function makeConfig(overrides: Partial = {}): GatewayConfig { runtimeTimeoutMs: 30000, runtimeMaxRetries: 2, runtimeInitialBackoffMs: 500, + telegramDeliverAuthBypass: false, telegramInitialBackoffMs: 1000, telegramMaxRetries: 3, telegramTimeoutMs: 15000, diff --git a/gateway/src/__tests__/runtime-client.test.ts b/gateway/src/__tests__/runtime-client.test.ts index fac7dc74583..281f8f76ea8 100644 --- a/gateway/src/__tests__/runtime-client.test.ts +++ b/gateway/src/__tests__/runtime-client.test.ts @@ -26,6 +26,7 @@ const makeConfig = (overrides: Partial = {}): GatewayConfig => ({ runtimeTimeoutMs: 30000, runtimeMaxRetries: 2, runtimeInitialBackoffMs: 500, + telegramDeliverAuthBypass: false, telegramInitialBackoffMs: 1000, telegramMaxRetries: 3, telegramTimeoutMs: 15000, diff --git a/gateway/src/__tests__/runtime-proxy-auth.test.ts b/gateway/src/__tests__/runtime-proxy-auth.test.ts index 551a8cc1018..2b83cc6f205 100644 --- a/gateway/src/__tests__/runtime-proxy-auth.test.ts +++ b/gateway/src/__tests__/runtime-proxy-auth.test.ts @@ -22,6 +22,7 @@ function makeConfig(overrides: Partial = {}): GatewayConfig { runtimeTimeoutMs: 30000, runtimeMaxRetries: 2, runtimeInitialBackoffMs: 500, + telegramDeliverAuthBypass: false, telegramInitialBackoffMs: 1000, telegramMaxRetries: 3, telegramTimeoutMs: 15000, diff --git a/gateway/src/__tests__/runtime-proxy.test.ts b/gateway/src/__tests__/runtime-proxy.test.ts index 211709ac5a0..5cd3d28be62 100644 --- a/gateway/src/__tests__/runtime-proxy.test.ts +++ b/gateway/src/__tests__/runtime-proxy.test.ts @@ -20,6 +20,7 @@ function makeConfig(overrides: Partial = {}): GatewayConfig { runtimeTimeoutMs: 30000, runtimeMaxRetries: 2, runtimeInitialBackoffMs: 500, + telegramDeliverAuthBypass: false, telegramInitialBackoffMs: 1000, telegramMaxRetries: 3, telegramTimeoutMs: 15000, diff --git a/gateway/src/__tests__/telegram-deliver-auth.test.ts b/gateway/src/__tests__/telegram-deliver-auth.test.ts index c67e310fe50..e8e28e7e682 100644 --- a/gateway/src/__tests__/telegram-deliver-auth.test.ts +++ b/gateway/src/__tests__/telegram-deliver-auth.test.ts @@ -22,6 +22,7 @@ function makeConfig(overrides: Partial = {}): GatewayConfig { runtimeTimeoutMs: 30000, runtimeMaxRetries: 2, runtimeInitialBackoffMs: 500, + telegramDeliverAuthBypass: false, telegramInitialBackoffMs: 1000, telegramMaxRetries: 3, telegramTimeoutMs: 15000, @@ -78,7 +79,7 @@ describe("/deliver/telegram attachment delivery without assistantId", () => { }) as any; const handler = createTelegramDeliverHandler( - makeConfig({ runtimeProxyBearerToken: undefined }), + makeConfig({ runtimeProxyBearerToken: undefined, telegramDeliverAuthBypass: true }), ); const req = new Request("http://localhost:7830/deliver/telegram", { method: "POST", @@ -131,7 +132,7 @@ describe("/deliver/telegram attachment delivery without assistantId", () => { }) as any; const handler = createTelegramDeliverHandler( - makeConfig({ runtimeProxyBearerToken: undefined }), + makeConfig({ runtimeProxyBearerToken: undefined, telegramDeliverAuthBypass: true }), ); const req = new Request("http://localhost:7830/deliver/telegram", { method: "POST", @@ -222,8 +223,7 @@ describe("/deliver/telegram bearer auth enforcement", () => { expect(body.ok).toBe(true); }); - test("allows unauthenticated access when no token is configured", async () => { - mockTelegramApi(); + test("returns 503 when no token is configured and bypass is not set", async () => { const handler = createTelegramDeliverHandler( makeConfig({ runtimeProxyBearerToken: undefined }), ); @@ -234,11 +234,45 @@ describe("/deliver/telegram bearer auth enforcement", () => { }); const res = await handler(req); + expect(res.status).toBe(503); + const body = await res.json(); + expect(body.error).toBe("Service not configured: bearer token required"); + }); + + test("allows unauthenticated access when bypass flag is set and no token configured", async () => { + mockTelegramApi(); + const handler = createTelegramDeliverHandler( + makeConfig({ runtimeProxyBearerToken: undefined, telegramDeliverAuthBypass: true }), + ); + const req = new Request("http://localhost:7830/deliver/telegram", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ chatId: "123", text: "hello" }), + }); + const res = await handler(req); + expect(res.status).toBe(200); const body = await res.json(); expect(body.ok).toBe(true); }); + test("bypass flag is ignored when a bearer token is configured (auth still required)", async () => { + const handler = createTelegramDeliverHandler( + makeConfig({ telegramDeliverAuthBypass: true }), + ); + const req = new Request("http://localhost:7830/deliver/telegram", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ chatId: "123", text: "hello" }), + }); + const res = await handler(req); + + // Token is configured, so missing Authorization header is still rejected + expect(res.status).toBe(401); + const body = await res.json(); + expect(body.error).toBe("Unauthorized"); + }); + test("still rejects non-POST methods before auth check", async () => { const handler = createTelegramDeliverHandler(makeConfig()); const req = new Request("http://localhost:7830/deliver/telegram", { diff --git a/gateway/src/__tests__/telegram-send-attachments.test.ts b/gateway/src/__tests__/telegram-send-attachments.test.ts index 9162ce5eb2e..4fc310e68bd 100644 --- a/gateway/src/__tests__/telegram-send-attachments.test.ts +++ b/gateway/src/__tests__/telegram-send-attachments.test.ts @@ -20,6 +20,7 @@ const makeConfig = (overrides: Partial = {}): GatewayConfig => ({ runtimeTimeoutMs: 30000, runtimeMaxRetries: 2, runtimeInitialBackoffMs: 500, + telegramDeliverAuthBypass: false, telegramInitialBackoffMs: 1000, telegramMaxRetries: 3, telegramTimeoutMs: 15000, diff --git a/gateway/src/__tests__/telegram-webhook-manager.test.ts b/gateway/src/__tests__/telegram-webhook-manager.test.ts index 7a2fea348cc..8cf2839baa3 100644 --- a/gateway/src/__tests__/telegram-webhook-manager.test.ts +++ b/gateway/src/__tests__/telegram-webhook-manager.test.ts @@ -20,6 +20,7 @@ function makeConfig(overrides: Partial = {}): GatewayConfig { runtimeTimeoutMs: 30000, runtimeMaxRetries: 2, runtimeInitialBackoffMs: 500, + telegramDeliverAuthBypass: false, telegramInitialBackoffMs: 1000, telegramMaxRetries: 0, telegramTimeoutMs: 15000, diff --git a/gateway/src/__tests__/twilio-relay-websocket.test.ts b/gateway/src/__tests__/twilio-relay-websocket.test.ts index 95a5dc95f01..eebd74944b3 100644 --- a/gateway/src/__tests__/twilio-relay-websocket.test.ts +++ b/gateway/src/__tests__/twilio-relay-websocket.test.ts @@ -34,6 +34,7 @@ const makeConfig = (overrides: Partial = {}): GatewayConfig => ({ runtimeTimeoutMs: 30000, runtimeMaxRetries: 2, runtimeInitialBackoffMs: 500, + telegramDeliverAuthBypass: false, telegramInitialBackoffMs: 1000, telegramMaxRetries: 3, telegramTimeoutMs: 15000, diff --git a/gateway/src/__tests__/twilio-webhooks.test.ts b/gateway/src/__tests__/twilio-webhooks.test.ts index 9130811b634..f725af60a34 100644 --- a/gateway/src/__tests__/twilio-webhooks.test.ts +++ b/gateway/src/__tests__/twilio-webhooks.test.ts @@ -24,6 +24,7 @@ const makeConfig = (overrides: Partial = {}): GatewayConfig => ({ runtimeTimeoutMs: 30000, runtimeMaxRetries: 2, runtimeInitialBackoffMs: 500, + telegramDeliverAuthBypass: false, telegramInitialBackoffMs: 1000, telegramMaxRetries: 3, telegramTimeoutMs: 15000, diff --git a/gateway/src/config.ts b/gateway/src/config.ts index de65afe08f9..da835d57298 100644 --- a/gateway/src/config.ts +++ b/gateway/src/config.ts @@ -31,6 +31,11 @@ export type GatewayConfig = { shutdownDrainMs: number; telegramApiBaseUrl: string; telegramBotToken: string | undefined; + /** + * When true, the /deliver/telegram endpoint allows unauthenticated access + * even when no bearer token is configured. Intended for local development only. + */ + telegramDeliverAuthBypass: boolean; telegramInitialBackoffMs: number; telegramMaxRetries: number; telegramTimeoutMs: number; @@ -173,6 +178,18 @@ export function loadConfig(): GatewayConfig { throw new Error("GATEWAY_RUNTIME_INITIAL_BACKOFF_MS must be a positive number"); } + const telegramDeliverAuthBypassRaw = process.env.GATEWAY_TELEGRAM_DELIVER_AUTH_BYPASS; + if ( + telegramDeliverAuthBypassRaw !== undefined && + telegramDeliverAuthBypassRaw !== "true" && + telegramDeliverAuthBypassRaw !== "false" + ) { + throw new Error( + `GATEWAY_TELEGRAM_DELIVER_AUTH_BYPASS must be "true" or "false", got "${telegramDeliverAuthBypassRaw}"`, + ); + } + const telegramDeliverAuthBypass = telegramDeliverAuthBypassRaw === "true"; + const telegramTimeoutMs = Number(process.env.GATEWAY_TELEGRAM_TIMEOUT_MS || "15000"); if (!Number.isFinite(telegramTimeoutMs) || telegramTimeoutMs <= 0) { throw new Error("GATEWAY_TELEGRAM_TIMEOUT_MS must be a positive number"); @@ -240,6 +257,7 @@ export function loadConfig(): GatewayConfig { port, runtimeProxyEnabled, runtimeProxyRequireAuth, + telegramDeliverAuthBypass, hasTwilioAuthToken: !!twilioAuthToken, publicUrl, }, @@ -265,6 +283,7 @@ export function loadConfig(): GatewayConfig { shutdownDrainMs, telegramApiBaseUrl, telegramBotToken, + telegramDeliverAuthBypass, telegramInitialBackoffMs, telegramMaxRetries, telegramTimeoutMs, diff --git a/gateway/src/http/routes/telegram-deliver.ts b/gateway/src/http/routes/telegram-deliver.ts index 6ac0d7cb05f..f379759eb60 100644 --- a/gateway/src/http/routes/telegram-deliver.ts +++ b/gateway/src/http/routes/telegram-deliver.ts @@ -12,9 +12,19 @@ export function createTelegramDeliverHandler(config: GatewayConfig) { return Response.json({ error: "Method not allowed" }, { status: 405 }); } - // Require bearer auth when a token is configured, preventing unauthenticated - // public access to the delivery endpoint. - if (config.runtimeProxyBearerToken) { + // Fail-closed auth: when no bearer token is configured and the explicit + // dev-only bypass flag is not set, refuse to serve requests (503) rather + // than silently allowing unauthenticated access. + if (!config.runtimeProxyBearerToken) { + if (config.telegramDeliverAuthBypass) { + // Dev-only bypass — skip auth entirely. + } else { + return Response.json( + { error: "Service not configured: bearer token required" }, + { status: 503 }, + ); + } + } else { const authResult = validateBearerToken( req.headers.get("authorization"), config.runtimeProxyBearerToken,