@yaebal/web 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 neverlane
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # @yaebal/web
2
+
3
+ run your [yaebal](https://github.com/neverlane/yaebal) bot on edge/web runtimes —
4
+ cloudflare workers, deno deploy, bun, vercel edge — via webhooks. `fetch`-first,
5
+ zero `node:` imports.
6
+
7
+ ## install
8
+
9
+ ```sh
10
+ pnpm add @yaebal/web
11
+ ```
12
+
13
+ ## usage
14
+
15
+ ```ts
16
+ import { Bot } from "@yaebal/core";
17
+ import { webhook, setWebhook } from "@yaebal/web";
18
+
19
+ // cloudflare workers / deno deploy / vercel edge
20
+ export default {
21
+ fetch(request: Request, env: { BOT_TOKEN: string; SECRET: string }) {
22
+ const bot = new Bot(env.BOT_TOKEN);
23
+
24
+ bot.command("start", (ctx) => ctx.reply("running on the edge ⚡"));
25
+
26
+ return webhook(bot, { secretToken: env.SECRET })(request);
27
+ },
28
+ };
29
+
30
+ // register once on deploy
31
+ await setWebhook(bot, "https://my-worker.workers.dev/", { secretToken: SECRET });
32
+ ```
33
+
34
+ `serve(bot, { port })` starts a native fetch server on bun or deno. on bode, use
35
+ `nodeWebhookCallback` from `@yaebal/core`. the operator dashboard lives in
36
+ [`@yaebal/panel`](https://github.com/neverlane/yaebal/tree/master/packages/panel).
37
+
38
+ ---
39
+
40
+ part of [**yaebal**](https://github.com/neverlane/yaebal) — a type-safe, runtime-agnostic Telegram Bot API framework. MIT.
package/lib/index.d.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { type UpdateSink, type WebhookOptions } from "@yaebal/core";
2
+ /**
3
+ * @yaebal/web — run your bot on edge/web runtimes (cloudflare workers, deno
4
+ * deploy, bun, vercel edge) via webhooks. the bot needs no long-polling: it just
5
+ * turns each incoming `Request` into an update.
6
+ *
7
+ * // cloudflare workers / deno deploy / vercel edge
8
+ * export default { fetch: webhook(bot, { secretToken: env.SECRET }) };
9
+ *
10
+ * // bun / deno standalone
11
+ * serve(bot, { port: 8080, secretToken: process.env.SECRET });
12
+ */
13
+ export type { WebhookOptions, UpdateSink } from "@yaebal/core";
14
+ /** minimal bot surface the lifecycle helpers need. */
15
+ interface ApiBot {
16
+ api: {
17
+ call(method: string, params?: Record<string, unknown>): Promise<unknown>;
18
+ };
19
+ }
20
+ /**
21
+ * a fetch handler — `(Request) => Promise<Response>` — for any edge/web runtime.
22
+ * drop it into a worker's `fetch`, `Deno.serve`, `Bun.serve`, etc.
23
+ */
24
+ export declare function webhook(bot: UpdateSink, options?: WebhookOptions): (request: Request) => Promise<Response>;
25
+ export interface SetWebhookOptions {
26
+ /** secret token telegram echoes back in `X-Telegram-Bot-Api-Secret-Token` (pass the same to `webhook`). */
27
+ secretToken?: string;
28
+ /** restrict the update types telegram sends. */
29
+ allowedUpdates?: string[];
30
+ /** drop updates that piled up while the webhook was unset. */
31
+ dropPendingUpdates?: boolean;
32
+ /** max simultaneous HTTPS connections (1–100). */
33
+ maxConnections?: number;
34
+ }
35
+ /** register this bot's webhook with telegram — call once on deploy. */
36
+ export declare function setWebhook(bot: ApiBot, url: string, options?: SetWebhookOptions): Promise<void>;
37
+ /** remove this bot's webhook (e.g. to switch back to long-polling). */
38
+ export declare function deleteWebhook(bot: ApiBot, dropPendingUpdates?: boolean): Promise<void>;
39
+ export interface ServeOptions extends WebhookOptions {
40
+ /** listen port. defaults to 8080. */
41
+ port?: number;
42
+ /** listen hostname. */
43
+ hostname?: string;
44
+ }
45
+ /**
46
+ * start a standalone webhook server using the runtime's native fetch server
47
+ * (bun or deno). on node use `nodeWebhookCallback` from `@yaebal/core`; on edge,
48
+ * export `{ fetch: webhook(bot) }` instead.
49
+ */
50
+ export declare function serve(bot: UpdateSink, options?: ServeOptions): void;
51
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,cAAc,EAAmB,MAAM,cAAc,CAAC;AAErF;;;;;;;;;;GAUG;AAEH,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/D,sDAAsD;AACtD,UAAU,MAAM;IACf,GAAG,EAAE;QAAE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC;CAClF;AAED;;;GAGG;AACH,wBAAgB,OAAO,CACtB,GAAG,EAAE,UAAU,EACf,OAAO,CAAC,EAAE,cAAc,GACtB,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAEzC;AAED,MAAM,WAAW,iBAAiB;IACjC,2GAA2G;IAC3G,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,kDAAkD;IAClD,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,uEAAuE;AACvE,wBAAsB,UAAU,CAC/B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,iBAAsB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAQf;AAED,uEAAuE;AACvE,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,kBAAkB,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1F;AAED,MAAM,WAAW,YAAa,SAAQ,cAAc;IACnD,qCAAqC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uBAAuB;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAYD;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,UAAU,EAAE,OAAO,GAAE,YAAiB,GAAG,IAAI,CAavE"}
package/lib/index.js ADDED
@@ -0,0 +1,41 @@
1
+ import { webhookCallback } from "@yaebal/core";
2
+ /**
3
+ * a fetch handler — `(Request) => Promise<Response>` — for any edge/web runtime.
4
+ * drop it into a worker's `fetch`, `Deno.serve`, `Bun.serve`, etc.
5
+ */
6
+ export function webhook(bot, options) {
7
+ return webhookCallback(bot, options);
8
+ }
9
+ /** register this bot's webhook with telegram — call once on deploy. */
10
+ export async function setWebhook(bot, url, options = {}) {
11
+ await bot.api.call("setWebhook", {
12
+ url,
13
+ secret_token: options.secretToken,
14
+ allowed_updates: options.allowedUpdates,
15
+ drop_pending_updates: options.dropPendingUpdates,
16
+ max_connections: options.maxConnections,
17
+ });
18
+ }
19
+ /** remove this bot's webhook (e.g. to switch back to long-polling). */
20
+ export async function deleteWebhook(bot, dropPendingUpdates = false) {
21
+ await bot.api.call("deleteWebhook", { drop_pending_updates: dropPendingUpdates });
22
+ }
23
+ /**
24
+ * start a standalone webhook server using the runtime's native fetch server
25
+ * (bun or deno). on node use `nodeWebhookCallback` from `@yaebal/core`; on edge,
26
+ * export `{ fetch: webhook(bot) }` instead.
27
+ */
28
+ export function serve(bot, options = {}) {
29
+ const handler = webhook(bot, options);
30
+ const runtime = globalThis;
31
+ if (runtime.Bun) {
32
+ runtime.Bun.serve({ port: options.port ?? 8080, hostname: options.hostname, fetch: handler });
33
+ }
34
+ else if (runtime.Deno) {
35
+ runtime.Deno.serve({ port: options.port ?? 8080, hostname: options.hostname }, handler);
36
+ }
37
+ else {
38
+ throw new Error("serve() requires bun or deno. on node use nodeWebhookCallback from @yaebal/core; on edge export { fetch: webhook(bot) }.");
39
+ }
40
+ }
41
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwC,eAAe,EAAE,MAAM,cAAc,CAAC;AAqBrF;;;GAGG;AACH,MAAM,UAAU,OAAO,CACtB,GAAe,EACf,OAAwB;IAExB,OAAO,eAAe,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AACtC,CAAC;AAaD,uEAAuE;AACvE,MAAM,CAAC,KAAK,UAAU,UAAU,CAC/B,GAAW,EACX,GAAW,EACX,UAA6B,EAAE;IAE/B,MAAM,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE;QAChC,GAAG;QACH,YAAY,EAAE,OAAO,CAAC,WAAW;QACjC,eAAe,EAAE,OAAO,CAAC,cAAc;QACvC,oBAAoB,EAAE,OAAO,CAAC,kBAAkB;QAChD,eAAe,EAAE,OAAO,CAAC,cAAc;KACvC,CAAC,CAAC;AACJ,CAAC;AAED,uEAAuE;AACvE,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAW,EAAE,kBAAkB,GAAG,KAAK;IAC1E,MAAM,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,CAAC,CAAC;AACnF,CAAC;AAmBD;;;;GAIG;AACH,MAAM,UAAU,KAAK,CAAC,GAAe,EAAE,UAAwB,EAAE;IAChE,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,UAAsD,CAAC;IAEvE,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;QACjB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;IAC/F,CAAC;SAAM,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;QACzB,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,EAAE,OAAO,CAAC,CAAC;IACzF,CAAC;SAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACd,0HAA0H,CAC1H,CAAC;IACH,CAAC;AACF,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,51 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { deleteWebhook, setWebhook, webhook } from "./index.js";
4
+ const sink = () => {
5
+ const updates = [];
6
+ const bot = { handleUpdate: async (u) => void updates.push(u) };
7
+ return { bot, updates };
8
+ };
9
+ const post = (body, headers = {}) => new Request("https://example.dev/", { method: "POST", body: JSON.stringify(body), headers });
10
+ test("webhook handler dispatches POSTed updates", async () => {
11
+ const { bot, updates } = sink();
12
+ const res = await webhook(bot)(post({ update_id: 1, message: { text: "hi" } }));
13
+ assert.equal(res.status, 200);
14
+ assert.equal(updates.length, 1);
15
+ });
16
+ test("webhook handler rejects non-POST and bad secret", async () => {
17
+ const { bot, updates } = sink();
18
+ const handler = webhook(bot, { secretToken: "s3cret" });
19
+ const get = await handler(new Request("https://example.dev/", { method: "GET" }));
20
+ assert.equal(get.status, 405);
21
+ const bad = await handler(post({ update_id: 1 }, { "x-telegram-bot-api-secret-token": "nope" }));
22
+ assert.equal(bad.status, 401);
23
+ const ok = await handler(post({ update_id: 1 }, { "x-telegram-bot-api-secret-token": "s3cret" }));
24
+ assert.equal(ok.status, 200);
25
+ assert.equal(updates.length, 1);
26
+ });
27
+ test("setWebhook / deleteWebhook call the Bot API", async () => {
28
+ const calls = [];
29
+ const bot = {
30
+ api: {
31
+ call: async (method, params) => {
32
+ calls.push({ method, params });
33
+ return true;
34
+ },
35
+ },
36
+ };
37
+ await setWebhook(bot, "https://example.dev/hook", {
38
+ secretToken: "s3cret",
39
+ allowedUpdates: ["message"],
40
+ dropPendingUpdates: true,
41
+ });
42
+ assert.equal(calls[0]?.method, "setWebhook");
43
+ assert.equal(calls[0]?.params?.url, "https://example.dev/hook");
44
+ assert.equal(calls[0]?.params?.secret_token, "s3cret");
45
+ assert.deepEqual(calls[0]?.params?.allowed_updates, ["message"]);
46
+ assert.equal(calls[0]?.params?.drop_pending_updates, true);
47
+ await deleteWebhook(bot, true);
48
+ assert.equal(calls[1]?.method, "deleteWebhook");
49
+ assert.equal(calls[1]?.params?.drop_pending_updates, true);
50
+ });
51
+ //# sourceMappingURL=index.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@yaebal/web",
3
+ "version": "0.0.1",
4
+ "description": "yaebal web — run your bot on edge/web runtimes (cloudflare workers, deno, bun) via webhooks.",
5
+ "type": "module",
6
+ "main": "./lib/index.js",
7
+ "types": "./lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "import": "./lib/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "lib",
16
+ "src"
17
+ ],
18
+ "dependencies": {
19
+ "@yaebal/core": "0.0.1"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "latest"
23
+ },
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "keywords": [
28
+ "telegram",
29
+ "telegram-bot",
30
+ "yaebal",
31
+ "webhook",
32
+ "edge",
33
+ "cloudflare-workers",
34
+ "deno",
35
+ "bun"
36
+ ],
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/neverlane/yaebal",
41
+ "directory": "packages/web"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "scripts": {
47
+ "build": "tsc -p tsconfig.json",
48
+ "typecheck": "tsc -p tsconfig.json --noEmit",
49
+ "test": "node --test lib"
50
+ }
51
+ }
@@ -0,0 +1,65 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { deleteWebhook, setWebhook, webhook } from "./index.js";
4
+
5
+ const sink = () => {
6
+ const updates: unknown[] = [];
7
+ const bot = { handleUpdate: async (u: never) => void updates.push(u) };
8
+
9
+ return { bot, updates };
10
+ };
11
+
12
+ const post = (body: unknown, headers: Record<string, string> = {}) =>
13
+ new Request("https://example.dev/", { method: "POST", body: JSON.stringify(body), headers });
14
+
15
+ test("webhook handler dispatches POSTed updates", async () => {
16
+ const { bot, updates } = sink();
17
+
18
+ const res = await webhook(bot)(post({ update_id: 1, message: { text: "hi" } }));
19
+
20
+ assert.equal(res.status, 200);
21
+ assert.equal(updates.length, 1);
22
+ });
23
+
24
+ test("webhook handler rejects non-POST and bad secret", async () => {
25
+ const { bot, updates } = sink();
26
+ const handler = webhook(bot, { secretToken: "s3cret" });
27
+
28
+ const get = await handler(new Request("https://example.dev/", { method: "GET" }));
29
+ assert.equal(get.status, 405);
30
+
31
+ const bad = await handler(post({ update_id: 1 }, { "x-telegram-bot-api-secret-token": "nope" }));
32
+ assert.equal(bad.status, 401);
33
+
34
+ const ok = await handler(post({ update_id: 1 }, { "x-telegram-bot-api-secret-token": "s3cret" }));
35
+ assert.equal(ok.status, 200);
36
+ assert.equal(updates.length, 1);
37
+ });
38
+
39
+ test("setWebhook / deleteWebhook call the Bot API", async () => {
40
+ const calls: { method: string; params?: Record<string, unknown> }[] = [];
41
+ const bot = {
42
+ api: {
43
+ call: async (method: string, params?: Record<string, unknown>) => {
44
+ calls.push({ method, params });
45
+ return true;
46
+ },
47
+ },
48
+ };
49
+
50
+ await setWebhook(bot, "https://example.dev/hook", {
51
+ secretToken: "s3cret",
52
+ allowedUpdates: ["message"],
53
+ dropPendingUpdates: true,
54
+ });
55
+
56
+ assert.equal(calls[0]?.method, "setWebhook");
57
+ assert.equal(calls[0]?.params?.url, "https://example.dev/hook");
58
+ assert.equal(calls[0]?.params?.secret_token, "s3cret");
59
+ assert.deepEqual(calls[0]?.params?.allowed_updates, ["message"]);
60
+ assert.equal(calls[0]?.params?.drop_pending_updates, true);
61
+
62
+ await deleteWebhook(bot, true);
63
+ assert.equal(calls[1]?.method, "deleteWebhook");
64
+ assert.equal(calls[1]?.params?.drop_pending_updates, true);
65
+ });
package/src/index.ts ADDED
@@ -0,0 +1,99 @@
1
+ import { type UpdateSink, type WebhookOptions, webhookCallback } from "@yaebal/core";
2
+
3
+ /**
4
+ * @yaebal/web — run your bot on edge/web runtimes (cloudflare workers, deno
5
+ * deploy, bun, vercel edge) via webhooks. the bot needs no long-polling: it just
6
+ * turns each incoming `Request` into an update.
7
+ *
8
+ * // cloudflare workers / deno deploy / vercel edge
9
+ * export default { fetch: webhook(bot, { secretToken: env.SECRET }) };
10
+ *
11
+ * // bun / deno standalone
12
+ * serve(bot, { port: 8080, secretToken: process.env.SECRET });
13
+ */
14
+
15
+ export type { WebhookOptions, UpdateSink } from "@yaebal/core";
16
+
17
+ /** minimal bot surface the lifecycle helpers need. */
18
+ interface ApiBot {
19
+ api: { call(method: string, params?: Record<string, unknown>): Promise<unknown> };
20
+ }
21
+
22
+ /**
23
+ * a fetch handler — `(Request) => Promise<Response>` — for any edge/web runtime.
24
+ * drop it into a worker's `fetch`, `Deno.serve`, `Bun.serve`, etc.
25
+ */
26
+ export function webhook(
27
+ bot: UpdateSink,
28
+ options?: WebhookOptions,
29
+ ): (request: Request) => Promise<Response> {
30
+ return webhookCallback(bot, options);
31
+ }
32
+
33
+ export interface SetWebhookOptions {
34
+ /** secret token telegram echoes back in `X-Telegram-Bot-Api-Secret-Token` (pass the same to `webhook`). */
35
+ secretToken?: string;
36
+ /** restrict the update types telegram sends. */
37
+ allowedUpdates?: string[];
38
+ /** drop updates that piled up while the webhook was unset. */
39
+ dropPendingUpdates?: boolean;
40
+ /** max simultaneous HTTPS connections (1–100). */
41
+ maxConnections?: number;
42
+ }
43
+
44
+ /** register this bot's webhook with telegram — call once on deploy. */
45
+ export async function setWebhook(
46
+ bot: ApiBot,
47
+ url: string,
48
+ options: SetWebhookOptions = {},
49
+ ): Promise<void> {
50
+ await bot.api.call("setWebhook", {
51
+ url,
52
+ secret_token: options.secretToken,
53
+ allowed_updates: options.allowedUpdates,
54
+ drop_pending_updates: options.dropPendingUpdates,
55
+ max_connections: options.maxConnections,
56
+ });
57
+ }
58
+
59
+ /** remove this bot's webhook (e.g. to switch back to long-polling). */
60
+ export async function deleteWebhook(bot: ApiBot, dropPendingUpdates = false): Promise<void> {
61
+ await bot.api.call("deleteWebhook", { drop_pending_updates: dropPendingUpdates });
62
+ }
63
+
64
+ export interface ServeOptions extends WebhookOptions {
65
+ /** listen port. defaults to 8080. */
66
+ port?: number;
67
+ /** listen hostname. */
68
+ hostname?: string;
69
+ }
70
+
71
+ type FetchHandler = (request: Request) => Promise<Response>;
72
+
73
+ interface BunRuntime {
74
+ serve(options: { port?: number; hostname?: string; fetch: FetchHandler }): unknown;
75
+ }
76
+
77
+ interface DenoRuntime {
78
+ serve(options: { port?: number; hostname?: string }, handler: FetchHandler): unknown;
79
+ }
80
+
81
+ /**
82
+ * start a standalone webhook server using the runtime's native fetch server
83
+ * (bun or deno). on node use `nodeWebhookCallback` from `@yaebal/core`; on edge,
84
+ * export `{ fetch: webhook(bot) }` instead.
85
+ */
86
+ export function serve(bot: UpdateSink, options: ServeOptions = {}): void {
87
+ const handler = webhook(bot, options);
88
+ const runtime = globalThis as { Bun?: BunRuntime; Deno?: DenoRuntime };
89
+
90
+ if (runtime.Bun) {
91
+ runtime.Bun.serve({ port: options.port ?? 8080, hostname: options.hostname, fetch: handler });
92
+ } else if (runtime.Deno) {
93
+ runtime.Deno.serve({ port: options.port ?? 8080, hostname: options.hostname }, handler);
94
+ } else {
95
+ throw new Error(
96
+ "serve() requires bun or deno. on node use nodeWebhookCallback from @yaebal/core; on edge export { fetch: webhook(bot) }.",
97
+ );
98
+ }
99
+ }