@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.
- package/.dockerignore +7 -0
- package/.env.example +59 -0
- package/Dockerfile +44 -0
- package/README.md +186 -0
- package/bun.lock +391 -0
- package/eslint.config.mjs +23 -0
- package/knip.json +8 -0
- package/package.json +27 -0
- package/src/__tests__/bearer-auth.test.ts +40 -0
- package/src/__tests__/config.test.ts +236 -0
- package/src/__tests__/dedup-cache.test.ts +101 -0
- package/src/__tests__/load-guards.test.ts +86 -0
- package/src/__tests__/probes.test.ts +94 -0
- package/src/__tests__/reply-path.test.ts +51 -0
- package/src/__tests__/resolve-assistant.test.ts +118 -0
- package/src/__tests__/runtime-client.test.ts +228 -0
- package/src/__tests__/runtime-proxy-auth.test.ts +127 -0
- package/src/__tests__/runtime-proxy.test.ts +262 -0
- package/src/__tests__/schema.test.ts +128 -0
- package/src/__tests__/telegram-normalize.test.ts +303 -0
- package/src/__tests__/telegram-only-default.test.ts +134 -0
- package/src/__tests__/telegram-send-attachments.test.ts +185 -0
- package/src/cli/schema.ts +8 -0
- package/src/config.ts +254 -0
- package/src/dedup-cache.ts +104 -0
- package/src/handlers/handle-inbound.ts +104 -0
- package/src/http/auth/bearer.ts +34 -0
- package/src/http/routes/runtime-proxy.ts +143 -0
- package/src/http/routes/telegram-webhook.ts +272 -0
- package/src/index.ts +117 -0
- package/src/logger.ts +103 -0
- package/src/routing/resolve-assistant.ts +45 -0
- package/src/routing/types.ts +11 -0
- package/src/runtime/client.ts +212 -0
- package/src/schema.ts +383 -0
- package/src/telegram/api.ts +153 -0
- package/src/telegram/download.ts +63 -0
- package/src/telegram/normalize.ts +118 -0
- package/src/telegram/send.ts +107 -0
- package/src/telegram/verify.ts +17 -0
- package/src/types.ts +37 -0
- package/tsconfig.json +20 -0
package/.dockerignore
ADDED
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. |
|