Files
miti99bot/plans/260411-0853-telegram-bot-plugin-framework/phase-02-webhook-entrypoint.md
tiennm99 c4314f21df feat: scaffold plug-n-play telegram bot on cloudflare workers
grammY-based bot with a module plugin system loaded from the MODULES env
var. Three command visibility levels (public/protected/private) share a
unified command namespace with conflict detection at registry build.

- 4 initial modules (util, wordle, loldle, misc); util fully implemented,
  others are stubs proving the plugin system end-to-end
- util: /info (chat/thread/sender ids) + /help (pure renderer over the
  registry, HTML parse mode, escapes user-influenced strings)
- KVStore interface with CFKVStore and a per-module prefixing factory;
  getJSON/putJSON convenience helpers; other backends drop in via one file
- Webhook at POST /webhook with secret-token validation via grammY's
  webhookCallback; no admin HTTP surface
- Post-deploy register script (npm run deploy = wrangler deploy && node
  --env-file=.env.deploy scripts/register.js) for setWebhook and
  setMyCommands; --dry-run flag for preview
- 56 vitest unit tests across 7 suites covering registry, db wrapper,
  dispatcher, help renderer, validators, and HTML escaper
- biome for lint + format; phased implementation plan under plans/
2026-04-11 09:49:06 +07:00

4.6 KiB

Phase 02 — Webhook entrypoint

Overview

  • Priority: P1
  • Status: pending
  • Description: fetch handler with URL routing (/webhook, GET / health, 404 otherwise), memoized Bot instance, grammY webhook secret-token validation wired through webhookCallback. Webhook + command-menu registration with Telegram is handled OUT OF BAND via a post-deploy node script (phase-07) — the Worker itself exposes no admin surface.

Key Insights

  • Use "cloudflare-mod" adapter (NOT "cloudflare" — that's the legacy service-worker variant).
  • webhookCallback(bot, "cloudflare-mod", { secretToken }) delegates X-Telegram-Bot-Api-Secret-Token validation to grammY — no manual header parsing.
  • Bot instance must be memoized at module scope but lazily constructed (env not available at import time).
  • No admin HTTP surface on the Worker — setWebhook + setMyCommands run from a local node script at deploy time, not via the Worker.

Requirements

Functional

  • POST /webhook → delegate to webhookCallback. Wrong/missing secret → 401 (handled by grammY).
  • GET / → 200 "miti99bot ok" (health check, unauthenticated).
  • Anything else → 404.

Non-functional

  • Single fetch function, <80 LOC.
  • No top-level await.
  • No global state besides memoized Bot.

Architecture

Request
  │
  ▼
fetch(req, env, ctx)
  │
  ├── GET /            → 200 "ok"
  ├── POST /webhook    → webhookCallback(bot, "cloudflare-mod", {secretToken})(req)
  └── *                → 404

getBot(env) lazily constructs and memoizes the Bot, installs dispatcher middleware (from phase-04), and returns the instance.

Create

  • src/index.js (fetch handler + URL router)
  • src/bot.js (memoized getBot(env) factory — wires grammY middleware from registry/dispatcher)

Modify

  • none

Delete

  • none

Implementation Steps

  1. Create src/index.js:
    • Import getBot from ./bot.js.
    • Export default object with async fetch(request, env, ctx).
    • Parse new URL(request.url), switch on pathname.
    • For POST /webhook: return webhookCallback(getBot(env), "cloudflare-mod", { secretToken: env.TELEGRAM_WEBHOOK_SECRET })(request).
    • For GET /: return 200 "miti99bot ok".
    • Default: 404.
  2. Create src/bot.js:
    • Module-scope let botInstance = null.
    • export function getBot(env):
      • If botInstance exists, return it.
      • Construct new Bot(env.TELEGRAM_BOT_TOKEN).
      • installDispatcher(bot, env) — imported from src/modules/dispatcher.js (phase-04 — stub import now, real impl later).
      • Assign + return.
    • Temporary stub: if installDispatcher not yet implemented, create a placeholder function in src/modules/dispatcher.js that does nothing so this phase compiles.
  3. Env validation: on first getBot call, throw if TELEGRAM_BOT_TOKEN / TELEGRAM_WEBHOOK_SECRET / MODULES missing. Fail fast is a feature.
  4. npm run lint — fix any issues.
  5. wrangler dev — hit GET / locally, confirm 200. Hit POST /webhook without secret header, confirm 401.

Todo List

  • src/index.js fetch handler + URL router
  • src/bot.js memoized factory
  • Placeholder src/modules/dispatcher.js exporting installDispatcher(bot, env) no-op
  • Env var validation with clear error messages
  • Manual smoke test via wrangler dev

Success Criteria

  • GET / returns 200 "miti99bot ok".
  • POST /webhook without header → 401 (via grammY).
  • POST /webhook with correct X-Telegram-Bot-Api-Secret-Token header and a valid Telegram update JSON body → 200.
  • Unknown path → 404.

Risk Assessment

Risk Likelihood Impact Mitigation
Wrong adapter string breaks webhook Low High Pin "cloudflare-mod"; test with wrangler dev + curl
Memoized Bot leaks state between deploys Low Low Warm-restart resets module scope; documented behavior
Cold-start latency from first Bot() construction Med Low Acceptable for bot use case

Security Considerations

  • TELEGRAM_WEBHOOK_SECRET MUST be configured before enabling webhook in Telegram; grammY's secretToken option gives 401 on mismatch.
  • Worker has NO admin HTTP surface — no attack surface beyond /webhook (secret-gated by grammY) and the public health check.
  • Never log secrets, even on error paths.

Next Steps

  • Phase 03 creates the DB abstraction that modules will use in phase 04+.
  • Phase 04 replaces the dispatcher stub with real middleware.