@vellumai/vellum-gateway 0.1.7

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 (42) hide show
  1. package/.dockerignore +7 -0
  2. package/.env.example +59 -0
  3. package/Dockerfile +44 -0
  4. package/README.md +186 -0
  5. package/bun.lock +391 -0
  6. package/eslint.config.mjs +23 -0
  7. package/knip.json +8 -0
  8. package/package.json +27 -0
  9. package/src/__tests__/bearer-auth.test.ts +40 -0
  10. package/src/__tests__/config.test.ts +236 -0
  11. package/src/__tests__/dedup-cache.test.ts +101 -0
  12. package/src/__tests__/load-guards.test.ts +86 -0
  13. package/src/__tests__/probes.test.ts +94 -0
  14. package/src/__tests__/reply-path.test.ts +51 -0
  15. package/src/__tests__/resolve-assistant.test.ts +118 -0
  16. package/src/__tests__/runtime-client.test.ts +228 -0
  17. package/src/__tests__/runtime-proxy-auth.test.ts +127 -0
  18. package/src/__tests__/runtime-proxy.test.ts +262 -0
  19. package/src/__tests__/schema.test.ts +128 -0
  20. package/src/__tests__/telegram-normalize.test.ts +303 -0
  21. package/src/__tests__/telegram-only-default.test.ts +134 -0
  22. package/src/__tests__/telegram-send-attachments.test.ts +185 -0
  23. package/src/cli/schema.ts +8 -0
  24. package/src/config.ts +254 -0
  25. package/src/dedup-cache.ts +104 -0
  26. package/src/handlers/handle-inbound.ts +104 -0
  27. package/src/http/auth/bearer.ts +34 -0
  28. package/src/http/routes/runtime-proxy.ts +143 -0
  29. package/src/http/routes/telegram-webhook.ts +272 -0
  30. package/src/index.ts +117 -0
  31. package/src/logger.ts +103 -0
  32. package/src/routing/resolve-assistant.ts +45 -0
  33. package/src/routing/types.ts +11 -0
  34. package/src/runtime/client.ts +212 -0
  35. package/src/schema.ts +383 -0
  36. package/src/telegram/api.ts +153 -0
  37. package/src/telegram/download.ts +63 -0
  38. package/src/telegram/normalize.ts +118 -0
  39. package/src/telegram/send.ts +107 -0
  40. package/src/telegram/verify.ts +17 -0
  41. package/src/types.ts +37 -0
  42. package/tsconfig.json +20 -0
package/.dockerignore ADDED
@@ -0,0 +1,7 @@
1
+ node_modules
2
+ dist
3
+ .env
4
+ .env.*
5
+ !.env.example
6
+ *.log
7
+ .DS_Store
package/.env.example ADDED
@@ -0,0 +1,59 @@
1
+ # Required: Telegram bot token from @BotFather
2
+ TELEGRAM_BOT_TOKEN=
3
+
4
+ # Required: Secret token for verifying Telegram webhook requests
5
+ TELEGRAM_WEBHOOK_SECRET=
6
+
7
+ # Optional: Override Telegram API base URL (default: https://api.telegram.org)
8
+ # TELEGRAM_API_BASE_URL=https://api.telegram.org
9
+
10
+ # Required: Base URL of the assistant runtime HTTP server
11
+ ASSISTANT_RUNTIME_BASE_URL=http://localhost:7821
12
+
13
+ # Optional: JSON mapping of Telegram identities to assistant IDs
14
+ # Format: { "chat:<chat_id>": "<assistant_id>", "user:<user_id>": "<assistant_id>" }
15
+ # GATEWAY_ASSISTANT_ROUTING_JSON={}
16
+
17
+ # Optional: Default assistant ID when no explicit route matches
18
+ # GATEWAY_DEFAULT_ASSISTANT_ID=
19
+
20
+ # Optional: Policy for unmapped Telegram users ("reject" | "default"). Default: "reject"
21
+ # GATEWAY_UNMAPPED_POLICY=reject
22
+
23
+ # Optional: Port for the gateway HTTP server (default: 7830)
24
+ # GATEWAY_PORT=7830
25
+
26
+ # Optional: Enable runtime proxy mode (default: false)
27
+ # When enabled, non-Telegram requests are forwarded to the assistant runtime.
28
+ # GATEWAY_RUNTIME_PROXY_ENABLED=false
29
+
30
+ # Optional: Require bearer auth for proxied requests (default: true)
31
+ # Only relevant when proxy mode is enabled.
32
+ # GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH=true
33
+
34
+ # Required when proxy is enabled with auth required: Bearer token for proxy auth
35
+ # RUNTIME_PROXY_BEARER_TOKEN=
36
+
37
+ # Optional: Graceful shutdown drain window in milliseconds (default: 5000)
38
+ # GATEWAY_SHUTDOWN_DRAIN_MS=5000
39
+
40
+ # Optional: Timeout for runtime HTTP calls in milliseconds (default: 30000)
41
+ # GATEWAY_RUNTIME_TIMEOUT_MS=30000
42
+
43
+ # Optional: Max retries for runtime forward on 5xx/network errors (default: 2)
44
+ # GATEWAY_RUNTIME_MAX_RETRIES=2
45
+
46
+ # Optional: Initial backoff between retries in milliseconds (default: 500)
47
+ # GATEWAY_RUNTIME_INITIAL_BACKOFF_MS=500
48
+
49
+ # Optional: Timeout for Telegram API/download calls in milliseconds (default: 15000)
50
+ # GATEWAY_TELEGRAM_TIMEOUT_MS=15000
51
+
52
+ # Optional: Max inbound webhook payload size in bytes (default: 1048576 = 1 MB)
53
+ # GATEWAY_MAX_WEBHOOK_PAYLOAD_BYTES=1048576
54
+
55
+ # Optional: Max single attachment size in bytes (default: 20971520 = 20 MB)
56
+ # GATEWAY_MAX_ATTACHMENT_BYTES=20971520
57
+
58
+ # Optional: Max concurrent attachment download/upload operations (default: 3)
59
+ # GATEWAY_MAX_ATTACHMENT_CONCURRENCY=3
package/Dockerfile ADDED
@@ -0,0 +1,44 @@
1
+ # Build stage
2
+ FROM debian:bookworm AS builder
3
+
4
+ WORKDIR /app
5
+
6
+ RUN apt-get update && apt-get install -y \
7
+ curl \
8
+ unzip \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ RUN curl -fsSL https://bun.sh/install | bash
12
+ ENV PATH="/root/.bun/bin:${PATH}"
13
+
14
+ COPY package.json bun.lock ./
15
+ RUN bun install --frozen-lockfile --production
16
+
17
+ COPY . .
18
+
19
+ # Runtime stage
20
+ FROM debian:bookworm-slim AS runner
21
+
22
+ WORKDIR /app
23
+
24
+ RUN apt-get update && apt-get install -y \
25
+ curl \
26
+ unzip \
27
+ ca-certificates \
28
+ && rm -rf /var/lib/apt/lists/*
29
+
30
+ # Copy bun binary from builder instead of re-installing
31
+ COPY --from=builder /root/.bun/bin/bun /usr/local/bin/bun
32
+
33
+ RUN groupadd --system --gid 1001 gateway && \
34
+ useradd --system --uid 1001 --gid gateway --create-home gateway
35
+
36
+ COPY --from=builder --chown=gateway:gateway /app /app
37
+
38
+ USER gateway
39
+
40
+ EXPOSE 7830
41
+
42
+ ENV GATEWAY_PORT=7830
43
+
44
+ CMD ["bun", "run", "src/index.ts"]
package/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # Vellum Gateway
2
+
3
+ Standalone service that owns Telegram integration end-to-end and optionally acts as an authenticated reverse proxy for the assistant runtime.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ Telegram → gateway/ → Assistant Runtime (/v1/assistants/:id/channels/inbound) → gateway/ → Telegram
9
+
10
+ Client → gateway/ (Bearer auth) → Assistant Runtime (any path)
11
+ ```
12
+
13
+ The web app is **not** in the Telegram request path. When proxy mode is enabled, non-Telegram requests are forwarded to the assistant runtime with optional bearer token authentication.
14
+
15
+ ## Setup
16
+
17
+ ```bash
18
+ cd gateway
19
+ bun install
20
+ cp .env.example .env
21
+ # Edit .env with your configuration
22
+ bun run dev
23
+ ```
24
+
25
+ ## Configuration
26
+
27
+ | Variable | Required | Default | Description |
28
+ |----------|----------|---------|-------------|
29
+ | `TELEGRAM_BOT_TOKEN` | No | — | Bot token from @BotFather (Telegram disabled when unset) |
30
+ | `TELEGRAM_WEBHOOK_SECRET` | No | — | Secret for verifying webhook requests (Telegram disabled when unset) |
31
+ | `TELEGRAM_API_BASE_URL` | No | `https://api.telegram.org` | Override Telegram API base URL |
32
+ | `ASSISTANT_RUNTIME_BASE_URL` | Yes | — | Base URL of the assistant runtime HTTP server |
33
+ | `GATEWAY_ASSISTANT_ROUTING_JSON` | No | `{}` | JSON mapping of Telegram identities to assistant IDs |
34
+ | `GATEWAY_DEFAULT_ASSISTANT_ID` | No | — | Default assistant ID for unmapped users |
35
+ | `GATEWAY_UNMAPPED_POLICY` | No | `reject` | Policy for unmapped users: `reject` or `default` |
36
+ | `GATEWAY_PORT` | No | `7830` | Port for the gateway HTTP server |
37
+ | `GATEWAY_RUNTIME_PROXY_ENABLED` | No | `false` | Enable runtime proxy for non-Telegram requests |
38
+ | `GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH` | No | `true` | Require bearer auth for proxied requests |
39
+ | `RUNTIME_PROXY_BEARER_TOKEN` | Conditional | — | Bearer token for proxy auth (required when proxy + auth enabled) |
40
+ | `GATEWAY_SHUTDOWN_DRAIN_MS` | No | `5000` | Graceful shutdown drain window in milliseconds |
41
+ | `GATEWAY_RUNTIME_TIMEOUT_MS` | No | `30000` | Timeout for runtime HTTP calls (ms) |
42
+ | `GATEWAY_RUNTIME_MAX_RETRIES` | No | `2` | Max retries for runtime forward on 5xx/network errors |
43
+ | `GATEWAY_RUNTIME_INITIAL_BACKOFF_MS` | No | `500` | Initial backoff between retries (doubles each attempt) |
44
+ | `GATEWAY_TELEGRAM_TIMEOUT_MS` | No | `15000` | Timeout for Telegram API/download calls (ms) |
45
+ | `GATEWAY_MAX_WEBHOOK_PAYLOAD_BYTES` | No | `1048576` | Max inbound webhook payload size (rejects with 413) |
46
+ | `GATEWAY_MAX_ATTACHMENT_BYTES` | No | `20971520` | Max single attachment size (oversized are skipped) |
47
+ | `GATEWAY_MAX_ATTACHMENT_CONCURRENCY` | No | `3` | Max concurrent attachment download/upload operations |
48
+
49
+ ## Routing
50
+
51
+ v1 uses deterministic settings-based routing (no database):
52
+
53
+ 1. **chat_id match** — explicit `chat:<chat_id>` entry in routing JSON
54
+ 2. **user_id match** — explicit `user:<user_id>` entry in routing JSON
55
+ 3. **Unmapped policy** — `reject` (drop with message) or `default` (forward to `GATEWAY_DEFAULT_ASSISTANT_ID`)
56
+
57
+ ### Routing JSON format
58
+
59
+ ```json
60
+ {
61
+ "chat:12345": "assistant-id-a",
62
+ "user:67890": "assistant-id-b"
63
+ }
64
+ ```
65
+
66
+ ## Setting up the Telegram webhook
67
+
68
+ After deploying the gateway, register the webhook with Telegram using the `setWebhook` API method. Pass:
69
+ - `url` — your gateway URL, e.g. `https://your-host/webhooks/telegram`
70
+ - The verify value matching your `TELEGRAM_WEBHOOK_SECRET` env var
71
+ - `allowed_updates` — `["message", "edited_message"]`
72
+
73
+ See the [Telegram Bot API docs](https://core.telegram.org/bots/api#setwebhook) for the full API reference.
74
+
75
+ ## Default Mode: Telegram-Only
76
+
77
+ By default the gateway only serves the Telegram webhook endpoint (`/webhooks/telegram`). All other HTTP requests return `404`. The runtime proxy is **opt-in** — set `GATEWAY_RUNTIME_PROXY_ENABLED=true` to enable it. This behavior is enforced by automated tests.
78
+
79
+ ## Runtime Proxy Mode
80
+
81
+ When `GATEWAY_RUNTIME_PROXY_ENABLED=true`, the gateway forwards all non-Telegram HTTP requests to the assistant runtime at `ASSISTANT_RUNTIME_BASE_URL`. This allows the gateway to serve as a single ingress point for both Telegram and API traffic.
82
+
83
+ ### Auth behavior
84
+
85
+ By default (`GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH=true`), proxied requests must include a valid `Authorization: Bearer <token>` header matching `RUNTIME_PROXY_BEARER_TOKEN`. Set `GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH=false` to disable auth.
86
+
87
+ `OPTIONS` requests are always allowed without auth (CORS preflight). Telegram webhook requests use their own secret-based verification and are not affected by proxy auth.
88
+
89
+ ### Examples
90
+
91
+ ```bash
92
+ # Unauthorized (expect 401 when auth required)
93
+ curl -i http://localhost:7830/v1/assistants/test/health
94
+
95
+ # Authorized (expect 200)
96
+ curl -i \
97
+ -H "Authorization: Bearer $RUNTIME_PROXY_BEARER_TOKEN" \
98
+ http://localhost:7830/v1/assistants/test/health
99
+
100
+ # Telegram still uses webhook secret flow, not bearer auth
101
+ curl -i -X POST http://localhost:7830/webhooks/telegram
102
+ ```
103
+
104
+ ### Proxy details
105
+
106
+ - Method, path, query string, headers, and body are forwarded to upstream.
107
+ - Hop-by-hop headers (`connection`, `keep-alive`, `transfer-encoding`, etc.) are stripped from both request and response.
108
+ - The `host` header is not forwarded to upstream.
109
+ - Upstream connection failures return `502 Bad Gateway`.
110
+
111
+ ## Outbound Attachments (Telegram)
112
+
113
+ When the assistant includes attachments in a reply, the gateway downloads each attachment from the runtime API and delivers it to the Telegram chat:
114
+
115
+ - **Images** (`image/*` MIME types) are sent via `sendPhoto` (multipart form upload).
116
+ - **Other files** are sent via `sendDocument` (multipart form upload).
117
+ - **Oversized** attachments (exceeding `GATEWAY_MAX_ATTACHMENT_BYTES`, default 20 MB) are skipped and included in the partial-failure notice.
118
+ - **Partial failures** are handled gracefully: each attachment is attempted independently. If any fail, a single summary notice is sent to the chat listing the undelivered filenames.
119
+ - **Concurrency** is controlled by `GATEWAY_MAX_ATTACHMENT_CONCURRENCY` (default 3).
120
+
121
+ Text and attachments are sent separately — the text reply goes first via `sendMessage`, then each attachment follows.
122
+
123
+ ## Health & Readiness Probes
124
+
125
+ | Endpoint | Method | Behavior |
126
+ |----------|--------|----------|
127
+ | `/healthz` | GET | Always returns `200` while the process is alive |
128
+ | `/readyz` | GET | Returns `200` while accepting traffic; `503` during graceful shutdown drain |
129
+
130
+ On `SIGTERM` the gateway enters drain mode: `/readyz` begins returning `503` so the load balancer stops sending new traffic. After `GATEWAY_SHUTDOWN_DRAIN_MS` (default 5 s) the process exits.
131
+
132
+ ## Docker
133
+
134
+ ```bash
135
+ # Build
136
+ docker build -t vellum-gateway:local gateway
137
+
138
+ # Run (pass required env vars)
139
+ docker run --rm -p 7830:7830 \
140
+ -e TELEGRAM_BOT_TOKEN=... \
141
+ -e TELEGRAM_WEBHOOK_SECRET=... \
142
+ -e ASSISTANT_RUNTIME_BASE_URL=http://host.docker.internal:7821 \
143
+ vellum-gateway:local
144
+ ```
145
+
146
+ The image runs as non-root user `gateway` (uid 1001) and exposes port `7830`.
147
+
148
+ ## Development
149
+
150
+ ```bash
151
+ cd gateway
152
+ bun install
153
+ bun run typecheck # TypeScript type check (tsc --noEmit)
154
+ bun run test # Run test suite
155
+ ```
156
+
157
+ Both checks run in CI on every pull request touching `gateway/`.
158
+
159
+ ## CI/CD
160
+
161
+ | Workflow | Trigger | What it does |
162
+ |----------|---------|--------------|
163
+ | `ci-gateway.yml` | PR (`gateway/**`) | Typecheck + tests |
164
+ | `ci-gateway-image.yml` | PR (`gateway/**`) | Build Docker image + smoke check |
165
+ | `cd-gateway-image.yml` | Push to `main` (`gateway/**`) | Build + push image to GCR |
166
+
167
+ The CD workflow requires these GitHub repository variables:
168
+ - `GCP_WORKLOAD_IDENTITY_PROVIDER` — OIDC provider for keyless auth
169
+ - `GCP_SERVICE_ACCOUNT` — Service account with push permissions
170
+ - `GCP_PROJECT_ID` — GCP project ID
171
+ - `GATEWAY_IMAGE_NAME` — Image name (e.g. `vellum-gateway`)
172
+ - `GCP_REGISTRY_HOST` — Registry host (e.g. `gcr.io`)
173
+
174
+ ## Load Testing
175
+
176
+ See [`benchmarking/gateway/README.md`](../benchmarking/gateway/README.md) for load-test scripts and throughput targets.
177
+
178
+ ## Troubleshooting
179
+
180
+ | Symptom | Check |
181
+ |---------|-------|
182
+ | Telegram messages not arriving | Is the webhook registered? `curl https://api.telegram.org/bot<TOKEN>/getWebhookInfo` |
183
+ | 401 on webhook | Does `TELEGRAM_WEBHOOK_SECRET` match the `secret_token` in setWebhook? |
184
+ | "No route configured" replies | Add a routing entry or set `GATEWAY_UNMAPPED_POLICY=default` with a default assistant |
185
+ | Runtime errors | Is `ASSISTANT_RUNTIME_BASE_URL` reachable? Check runtime logs. |
186
+ | No reply from assistant | Is the assistant runtime processing messages? Check for `RUNTIME_HTTP_PORT` env var. |