webhooks

two webhook handlers — a fetch-style one and a node http one — both feeding the same handleUpdate entry point, with a constant-time secret check and a body-size cap.

polling vs webhooks

for development, long polling is simplest: bot.start() loops getUpdates and dispatches each update through handleUpdate, retrying after a short delay if a poll fails. it resolves only when bot.stop() is called.

polling.ts
// long polling — start() loops getUpdates and calls handleUpdate for each
const bot = new Bot(process.env.BOT_TOKEN!);
bot.on("message:text", (ctx) => ctx.reply(ctx.text));
await bot.start();   // resolves only when stop() is called

for production you usually want webhooks: Telegram POSTs each update to your URL, and you hand the request straight to a yaebal handler. no polling loop, and it scales to serverless runtimes.

webhookCallback (fetch)

webhookCallback returns a (Request) => Promise<Response> function — the shape every fetch-based runtime expects. it only accepts POST, parses the JSON update, and dispatches it.

webhook.ts
import { webhookCallback } from "@yaebal/core";

// (Request) => Promise<Response>
const handler = webhookCallback(bot, { secretToken: process.env.WEBHOOK_SECRET });

the secret check

when you set secretToken, the handler requires Telegram's X-Telegram-Bot-Api-Secret-Token header to match. the comparison is constant-time (node:crypto timingSafeEqual on equal-length buffers), so the check can't be timed.

conditionresponse
method is not POST405
secretToken set and header mismatched401
body larger than 1 MiB413
body is not valid JSON400
update dispatched200 ok
body size cap. Telegram updates are tiny, so the handler rejects anything over 1 MiB (MAX_BODY) to avoid memory abuse — the fetch handler checks content-length; the node handler counts bytes as they stream and destroys the request if it overflows.

nodeWebhookCallback (node http)

for a plain Node server, nodeWebhookCallback returns an (req, res) handler you can drop into http.createServer. same secret check and same body cap, streamed off the incoming request.

server.ts
// Node http
import { createServer } from "node:http";
import { Bot, nodeWebhookCallback } from "@yaebal/core";

const bot = new Bot(process.env.BOT_TOKEN!);
bot.on("message:text", (ctx) => ctx.reply(ctx.text));

createServer(
  nodeWebhookCallback(bot, { secretToken: process.env.WEBHOOK_SECRET }),
).listen(8080);

handleUpdate

both handlers ultimately call bot.handleUpdate(update), which builds a Context and runs the middleware chain, sending any thrown error to your onError handler. you can also call it directly from a custom HTTP layer.

handle.ts
// both handlers ultimately call bot.handleUpdate — the single-update entry point.
// the chain is realized (and frozen) on the first call, so register every
// middleware / plugin before the first handleUpdate or start.
await bot.handleUpdate(update);
register before the first update. the chain is realized and frozen on the first handleUpdate (or start), so attach all middleware and plugins before then.

deploy: Cloudflare Workers

worker.ts
// Cloudflare Workers
import { Bot, webhookCallback } from "@yaebal/core";

const bot = new Bot(env.BOT_TOKEN);
bot.command("start", (ctx) => ctx.reply("hi from the edge"));

export default {
  fetch: webhookCallback(bot, { secretToken: env.WEBHOOK_SECRET }),
};

deploy: Bun

server.ts
// Bun
import { Bot, webhookCallback } from "@yaebal/core";

const bot = new Bot(process.env.BOT_TOKEN!);
bot.on("message:text", (ctx) => ctx.reply(ctx.text));

Bun.serve({
  port: 8080,
  fetch: webhookCallback(bot, { secretToken: process.env.WEBHOOK_SECRET }),
});