Skip to content

feat(bot): TELEGRAM_API_ROOT + TELEGRAM_PROXY_SECRET for reverse-proxy setups#92

Open
avfirsov wants to merge 7 commits intogrinev:mainfrom
avfirsov:reverse-proxy-apiroot-and-secret
Open

feat(bot): TELEGRAM_API_ROOT + TELEGRAM_PROXY_SECRET for reverse-proxy setups#92
avfirsov wants to merge 7 commits intogrinev:mainfrom
avfirsov:reverse-proxy-apiroot-and-secret

Conversation

@avfirsov
Copy link
Copy Markdown
Contributor

@avfirsov avfirsov commented Apr 21, 2026

Summary

Adds two new environment variables for routing Telegram Bot API calls and file downloads through a custom HTTPS reverse-proxy (e.g. nginx proxying api.telegram.org) with an optional shared-secret header.

  • TELEGRAM_API_ROOT — replaces https://api.telegram.org for both Bot API calls (via grammY's client.apiRoot) and file downloads. Defaults to empty → existing behaviour preserved.
  • TELEGRAM_PROXY_SECRET — if set, X-Proxy-Secret header is sent on every request so the reverse proxy can authorise callers. Defaults to empty.

Motivation

Corporate networks frequently block api.telegram.org at the DNS/IP level but allow the operator's own HTTPS endpoint. The existing TELEGRAM_PROXY_URL covers the SOCKS/HTTP-CONNECT forward-proxy case (tunnel TCP to api.telegram.org via a proxy). This PR covers the orthogonal reverse-proxy / URL-rewrite case: the bot connects directly to a reverse proxy that forwards to api.telegram.org server-side, with a shared secret gating access.

Typical nginx config on the operator's VPS:

server {
    listen 443 ssl http2;
    server_name tg-proxy.yourdomain.com;

    ssl_certificate     /etc/letsencrypt/live/tg-proxy.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/tg-proxy.yourdomain.com/privkey.pem;

    access_log off;  # token is in URL path — don't log
    client_max_body_size 50m;

    if ($http_x_proxy_secret != "your-shared-secret") { return 403; }

    location / {
        proxy_pass https://api.telegram.org;
        proxy_ssl_server_name on;
        proxy_set_header Host api.telegram.org;
    }
}

Bot-side .env:

TELEGRAM_API_ROOT=https://tg-proxy.yourdomain.com
TELEGRAM_PROXY_SECRET=your-shared-secret

Changes

  • src/config.ts — new telegram.apiRoot + telegram.proxySecret fields.
  • src/bot/index.ts — pass apiRoot into grammY's client options; inject X-Proxy-Secret via baseFetchConfig.headers. Composes with the existing TELEGRAM_PROXY_URL path.
  • src/bot/utils/file-download.ts — derive the file URL base from apiRoot; add the secret header to the fetch() call.
  • src/bot/handlers/voice.ts — same for the raw http.get() code path that downloads voice files (URL base + header).
  • .env.example — documents the two new variables with a short motivation.

Note on branch contents

This branch also carries a second commit that pins remark to 14.0.3 to fix an unrelated ERR_REQUIRE_ESM crash on Node 20 — I've opened that as a separate single-commit PR #93 so it can be reviewed and merged independently on its own merits. If #93 lands first, GitHub will auto-close that commit here and this PR will rebase cleanly. If you prefer I drop the remark commit from this branch while #93 is under review, let me know and I'll force-push.

Backward compatibility

Both new env vars default to empty. When unset, the bot behaves exactly as before: base URL stays https://api.telegram.org, no extra headers, no logs. Existing installs are unaffected.

Test plan

  • With neither var set — behavior unchanged, bot operates against api.telegram.org directly.
  • With TELEGRAM_API_ROOT set — Bot API calls go to the new root; file downloads use the new root.
  • With TELEGRAM_PROXY_SECRET set — header is attached to API calls AND to both file-download code paths.
  • End-to-end test against an nginx reverse-proxy + if ($http_x_proxy_secret != "suckit") { return 403; } — getMe/sendMessage/getFile all round-trip correctly.

Notes

Composition with TELEGRAM_PROXY_URL: the reverse-proxy and forward-proxy features are orthogonal and can be used together (forward proxy to reach the reverse proxy, though that's an unusual combination). No code path conflict.

@grinev
Copy link
Copy Markdown
Owner

grinev commented Apr 21, 2026

@avfirsov thanks for contribution! Please update readme file and add new env variables there. Also these two variables should be near TELEGRAM_PROXY_URL in env.example

avfirsov and others added 6 commits April 21, 2026 12:25
telegram-markdown-v2 (our markdown-to-TG converter) does 'require("remark")'
in its compiled CJS output, but remark 15.0.0+ is ESM-only and Node refuses
require() of ESM modules with ERR_REQUIRE_ESM.

Pinning remark to 14.0.3 (the last CJS release) in 'overrides' fixes install
for any consumer of this package without changing telegram-markdown-v2.
Previous implementation put the header in client.baseFetchConfig.headers,
but grammY's ApiClient composes the per-request init as
  { ...opts.baseFetchConfig, signal, ...config }
where config.headers (Content-Type/Content-Length) wipes out anything set
on baseFetchConfig.headers via the shallow spread. The header never
reached the wire.

Switch to a custom client.fetch wrapper that clones per-request headers
and adds X-Proxy-Secret on top — same semantic as intended but survives
grammY's config merge.
The previous commit imported node-fetch via default import and used the
DOM Headers class + HeadersInit type. Both caused TS build errors
(TS7016 for node-fetch missing @types, TS2304 for HeadersInit not in the
Node default lib):

  src/bot/index.ts:1:38 - error TS7016: Could not find a declaration file
    for module 'node-fetch'. ...
  src/bot/index.ts:996:62 - error TS2304: Cannot find name 'HeadersInit'.

Switch to require() for node-fetch (cast to any; it's only used at runtime
from the proxy-secret code path) and a plain-object headers merge — no
more Headers class, no HeadersInit, no @types/node-fetch needed.

Runtime behaviour identical; only TS build gripes fixed.
The previous fix used require("node-fetch"), which throws
  CLI error: require is not defined
at runtime because this package sets "type": "module" in package.json
and runs as an ES module — require is not defined in ESM scopes.

Switch back to a static ESM import with @ts-expect-error to silence the
missing @types/node-fetch warning. Works on Node 22 and 25 alike, no
dynamic import(), no runtime module-lookup overhead.
* move TELEGRAM_API_ROOT / TELEGRAM_PROXY_SECRET in .env.example to sit
  next to TELEGRAM_PROXY_URL (per @grinev review)
* document both env vars in README.md (table + new "Reverse Proxy"
  subsection with nginx example) (per @grinev review)
* drop package.json `overrides.remark` block — telegram-markdown-v2 is
  gone from main, ERR_REQUIRE_ESM no longer reachable, override is dead
* rebase config.ts / index.ts / file-download.ts / voice.ts on current
  main; reverse-proxy logic itself unchanged
Per @grinev review on PR grinev#92:

* Add both env vars to the Environment Variables table, in the Telegram
  block right after TELEGRAM_PROXY_URL.
* Add a new "Reverse Proxy (Optional)" subsection under Configuration
  with motivation, .env snippet, and a copy-pasteable nginx config.
* Resync README.md to current main (gains /detach, /ls, /mcps,
  TTS provider columns, OPENCODE_AUTO_RESTART_ENABLED, etc.) so the
  diff is clean against main.
@avfirsov
Copy link
Copy Markdown
Contributor Author

avfirsov commented May 9, 2026

@grinev thanks for the review — addressed both points and rebased on current main:

Review feedback

  • .env.example — moved the TELEGRAM_API_ROOT / TELEGRAM_PROXY_SECRET block right after TELEGRAM_PROXY_URL (it now sits in the Telegram block instead of dangling at the end of the file). diff
  • README.md — both vars are now in the Environment Variables table next to TELEGRAM_PROXY_URL, plus a new Reverse Proxy (Optional) subsection under Configuration with motivation, an .env snippet, and a copy-pasteable nginx config (essentially the example from the PR description, lifted into the README so it's discoverable). diff

Drive-by — rebased the branch

Branch was dirty because:

  • fix(deps): pin remark to 14.0.3 via overrides (fix ERR_REQUIRE_ESM on Node 20) #93 (the remark pin) was merged in the meantime, so overrides.remark is dead — telegram-markdown-v2 is gone from main's deps and ERR_REQUIRE_ESM is no longer reachable. Dropped the overrides block.
  • main advanced significantly (v0.17.0 → v0.20.1, added OPENCODE_AUTO_RESTART_ENABLED, TRACK_BACKGROUND_SESSIONS, Google Cloud TTS, /ls /detach /mcps commands, etc.). Resynced package.json, .env.example, README.md to current main so the diff is clean and only contains the reverse-proxy-related additions.

Source files (src/config.ts, src/bot/index.ts, src/bot/handlers/voice.ts, src/bot/utils/file-download.ts) didn't need any rebase — main hadn't touched the lines I patched, so the original implementation applies to current main unchanged.

PR is now mergeable and CI (Lint, Build, Test) is green on the rebased head. Let me know if you'd like the description updated or anything else tweaked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants