easy-ping 0.6.1 → 0.7.0

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.
Files changed (56) hide show
  1. package/README.md +20 -4
  2. package/dist/{adapter-D3lTy-4E.d.cts → adapter-BJ8Iejvs.d.cts} +1 -1
  3. package/dist/{adapter-D3lTy-4E.d.ts → adapter-BJ8Iejvs.d.ts} +1 -1
  4. package/dist/adapters/drizzle.d.cts +1 -1
  5. package/dist/adapters/drizzle.d.ts +1 -1
  6. package/dist/adapters/mongodb.d.cts +2 -2
  7. package/dist/adapters/mongodb.d.ts +2 -2
  8. package/dist/adapters/mysql.d.cts +2 -2
  9. package/dist/adapters/mysql.d.ts +2 -2
  10. package/dist/adapters/postgres.d.cts +2 -2
  11. package/dist/adapters/postgres.d.ts +2 -2
  12. package/dist/adapters/sqlite.d.cts +2 -2
  13. package/dist/adapters/sqlite.d.ts +2 -2
  14. package/dist/chunk-R3BE67L7.js +13 -0
  15. package/dist/chunk-R3BE67L7.js.map +1 -0
  16. package/dist/chunk-SSHVZHSJ.js +100 -0
  17. package/dist/chunk-SSHVZHSJ.js.map +1 -0
  18. package/dist/index.d.cts +4 -4
  19. package/dist/index.d.ts +4 -4
  20. package/dist/index.js +2 -11
  21. package/dist/index.js.map +1 -1
  22. package/dist/{plugin-Cg9usuUi.d.ts → plugin-CSLKKgXT.d.ts} +1 -1
  23. package/dist/{plugin-CHd2ZiGC.d.cts → plugin-Wwp1nvo0.d.cts} +1 -1
  24. package/dist/plugins/digests.d.cts +2 -2
  25. package/dist/plugins/digests.d.ts +2 -2
  26. package/dist/plugins/preferences-client.d.cts +2 -2
  27. package/dist/plugins/preferences-client.d.ts +2 -2
  28. package/dist/plugins/preferences-react.d.cts +1 -1
  29. package/dist/plugins/preferences-react.d.ts +1 -1
  30. package/dist/plugins/preferences.cjs +1 -1
  31. package/dist/plugins/preferences.cjs.map +1 -1
  32. package/dist/plugins/preferences.d.cts +2 -2
  33. package/dist/plugins/preferences.d.ts +2 -2
  34. package/dist/plugins/preferences.js +1 -1
  35. package/dist/plugins/preferences.js.map +1 -1
  36. package/dist/plugins/push.d.cts +2 -2
  37. package/dist/plugins/push.d.ts +2 -2
  38. package/dist/plugins/telegram.cjs +379 -0
  39. package/dist/plugins/telegram.cjs.map +1 -0
  40. package/dist/plugins/telegram.d.cts +153 -0
  41. package/dist/plugins/telegram.d.ts +153 -0
  42. package/dist/plugins/telegram.js +313 -0
  43. package/dist/plugins/telegram.js.map +1 -0
  44. package/dist/providers/telegram.cjs +103 -0
  45. package/dist/providers/telegram.cjs.map +1 -0
  46. package/dist/providers/telegram.d.cts +83 -0
  47. package/dist/providers/telegram.d.ts +83 -0
  48. package/dist/providers/telegram.js +3 -0
  49. package/dist/providers/telegram.js.map +1 -0
  50. package/dist/providers/web-push.d.cts +2 -2
  51. package/dist/providers/web-push.d.ts +2 -2
  52. package/dist/schema.d.cts +2 -2
  53. package/dist/schema.d.ts +2 -2
  54. package/dist/testing.d.cts +1 -1
  55. package/dist/testing.d.ts +1 -1
  56. package/package.json +13 -2
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  # easy-ping
4
4
 
5
5
  **Own your notifications.**
6
- In-app inbox, transactional email and web push — running inside your app, stored in your
6
+ In-app inbox, transactional email, web push and Telegram — running inside your app, stored in your
7
7
  database, with no per-notification bill.
8
8
 
9
9
  [![npm](https://img.shields.io/npm/v/easy-ping?color=%23e0362a&label=npm)](https://www.npmjs.com/package/easy-ping)
@@ -47,7 +47,7 @@ hand-rolling it badly.
47
47
  | **Cost per notification** | none | none, you pay for servers | metered per send |
48
48
  | **License** | MIT, all of it | MIT core, commercial modules | proprietary |
49
49
  | **Visual workflow editor** | **no**, a notification is code | yes | yes |
50
- | **Channels out of the box** | **in-app, email, web push** | dozens | dozens |
50
+ | **Channels out of the box** | **in-app, email, web push, Telegram** | dozens | dozens |
51
51
 
52
52
  The last two rows are the trade. If you need a workflow editor a non-developer can edit, use one
53
53
  of the others — they are good tools solving a bigger problem.
@@ -56,7 +56,7 @@ of the others — they are good tools solving a bigger problem.
56
56
  Every "no queue required" decision below follows from that.
57
57
 
58
58
  > **Status: published, pre-1.0.** The core pipeline, all three adapters, the React client and the
59
- > preferences, digests and push plugins are covered by tests against real databases. Web push is
59
+ > preferences, digests, push and telegram plugins are covered by tests against real databases. Web push is
60
60
  > verified end to end against Mozilla's production push service and cross-checked against
61
61
  > `http_ece`. The bell updates over a server-sent event stream with polling as the fallback;
62
62
  > batching is not built, and the Resend provider has only been exercised against a stub. Minor
@@ -379,10 +379,25 @@ single replica.
379
379
  | `inApp` | ✅ built in, on by default, needs no provider |
380
380
  | `email` | ✅ Resend provider; the interface is open for others |
381
381
  | `push` | ✅ push plugin + `webPush()` — VAPID and aes128gcm on Web Crypto, so it runs on edge too |
382
+ | `telegram` | ✅ telegram plugin + `telegramBot()` — one-tap linking through the bot, webhook or long-poll |
382
383
  | `sms` | ⬜ not implemented |
383
384
  | `slack` | ⬜ not implemented |
384
385
 
385
- A channel is usable when core carries it (`inApp`, `email`) or a plugin declares it and can `deliver` it. That is how push works, and how sms and slack will.
386
+ A channel is usable when core carries it (`inApp`, `email`) or a plugin declares it and can `deliver` it. That is how push and telegram work, and how sms and slack will.
387
+
388
+ ```ts
389
+ import { telegram } from "easy-ping/plugins/telegram";
390
+ import { telegramBot } from "easy-ping/providers/telegram";
391
+
392
+ telegram({
393
+ provider: telegramBot({ token: process.env.TELEGRAM_BOT_TOKEN! }),
394
+ botUsername: "your_bot",
395
+ webhookSecret: process.env.TELEGRAM_WEBHOOK_SECRET, // or plugin.poll() in development
396
+ render: ({ payload }) => ({ text: `<b>${escapeHtml(payload.authorName)}</b> replied` }),
397
+ });
398
+ ```
399
+
400
+ `POST /telegram/link` returns a `t.me/<bot>?start=<code>` link; the user taps it, the bot stores the chat, and every `send()` with `telegram` in its channels reaches it. Blocked chats prune themselves.
386
401
 
387
402
  Declaring a channel nothing can carry **warns at startup** and reports `skipped: "channel-unavailable"` — deliberately distinct from `"no-channels"`, so a missing provider never looks like a user opt-out.
388
403
 
@@ -475,6 +490,7 @@ Defaults to the last 24 hours, capped at 1000 rows. Wire it to an admin page or
475
490
  | ✅ digests plugin, timezone-aware | |
476
491
  | ✅ push plugin + web-push provider | VAPID + RFC 8291, no node:crypto |
477
492
  | ✅ push verified against a live push service | Mozilla autopush, plus a cross-check against `http_ece` |
493
+ | ✅ telegram plugin + bot provider | one-tap linking, webhook or long-poll, blocked chats pruned |
478
494
  | ✅ scoped plugin storage, so plugins own their tables | |
479
495
  | ✅ additive schema migrations for the raw-SQL path | `planPostgresMigration()`; MySQL and SQLite are bootstrap-only for now |
480
496
  | ✅ failed deliveries reachable from the instance | `notify.getFailedDeliveries()` |
@@ -1,4 +1,4 @@
1
- type Channel = "inApp" | "email" | "push" | "sms" | "slack";
1
+ type Channel = "inApp" | "email" | "push" | "telegram" | "sms" | "slack";
2
2
  type DeliveryStatus = "pending" | "claimed" | "sent" | "failed" | "skipped";
3
3
  type DeliveryMode = "inline" | "deferred" | "worker" | "cron";
4
4
  type Frequency = "instant" | "daily" | "weekly" | "off";
@@ -1,4 +1,4 @@
1
- type Channel = "inApp" | "email" | "push" | "sms" | "slack";
1
+ type Channel = "inApp" | "email" | "push" | "telegram" | "sms" | "slack";
2
2
  type DeliveryStatus = "pending" | "claimed" | "sent" | "failed" | "skipped";
3
3
  type DeliveryMode = "inline" | "deferred" | "worker" | "cron";
4
4
  type Frequency = "instant" | "daily" | "weekly" | "off";
@@ -1,4 +1,4 @@
1
- import { D as DatabaseAdapter, C as Channel, a as DeliveryStatus, F as Frequency } from '../adapter-D3lTy-4E.cjs';
1
+ import { D as DatabaseAdapter, C as Channel, a as DeliveryStatus, F as Frequency } from '../adapter-BJ8Iejvs.cjs';
2
2
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
3
3
 
4
4
  type DrizzleAdapterOptions = {
@@ -1,4 +1,4 @@
1
- import { D as DatabaseAdapter, C as Channel, a as DeliveryStatus, F as Frequency } from '../adapter-D3lTy-4E.js';
1
+ import { D as DatabaseAdapter, C as Channel, a as DeliveryStatus, F as Frequency } from '../adapter-BJ8Iejvs.js';
2
2
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
3
3
 
4
4
  type DrizzleAdapterOptions = {
@@ -1,5 +1,5 @@
1
- import { D as DatabaseAdapter } from '../adapter-D3lTy-4E.cjs';
2
- import { S as SchemaDeclaration } from '../plugin-CHd2ZiGC.cjs';
1
+ import { D as DatabaseAdapter } from '../adapter-BJ8Iejvs.cjs';
2
+ import { S as SchemaDeclaration } from '../plugin-Wwp1nvo0.cjs';
3
3
  import { S as Signals } from '../signals-kPmOlKDX.cjs';
4
4
 
5
5
  type MongoAdapterOptions = {
@@ -1,5 +1,5 @@
1
- import { D as DatabaseAdapter } from '../adapter-D3lTy-4E.js';
2
- import { S as SchemaDeclaration } from '../plugin-Cg9usuUi.js';
1
+ import { D as DatabaseAdapter } from '../adapter-BJ8Iejvs.js';
2
+ import { S as SchemaDeclaration } from '../plugin-CSLKKgXT.js';
3
3
  import { S as Signals } from '../signals-kPmOlKDX.js';
4
4
 
5
5
  type MongoAdapterOptions = {
@@ -1,7 +1,7 @@
1
1
  import { S as SqlAdapterOptions, a as SqlExec } from '../adapter-B7R4rXtJ.cjs';
2
2
  export { b as MysqlResult } from '../adapter-B7R4rXtJ.cjs';
3
- import { D as DatabaseAdapter } from '../adapter-D3lTy-4E.cjs';
4
- import { S as SchemaDeclaration } from '../plugin-CHd2ZiGC.cjs';
3
+ import { D as DatabaseAdapter } from '../adapter-BJ8Iejvs.cjs';
4
+ import { S as SchemaDeclaration } from '../plugin-Wwp1nvo0.cjs';
5
5
 
6
6
  type MysqlAdapterOptions = SqlAdapterOptions;
7
7
  /**
@@ -1,7 +1,7 @@
1
1
  import { S as SqlAdapterOptions, a as SqlExec } from '../adapter-B7R4rXtJ.js';
2
2
  export { b as MysqlResult } from '../adapter-B7R4rXtJ.js';
3
- import { D as DatabaseAdapter } from '../adapter-D3lTy-4E.js';
4
- import { S as SchemaDeclaration } from '../plugin-Cg9usuUi.js';
3
+ import { D as DatabaseAdapter } from '../adapter-BJ8Iejvs.js';
4
+ import { S as SchemaDeclaration } from '../plugin-CSLKKgXT.js';
5
5
 
6
6
  type MysqlAdapterOptions = SqlAdapterOptions;
7
7
  /**
@@ -1,5 +1,5 @@
1
- import { D as DatabaseAdapter } from '../adapter-D3lTy-4E.cjs';
2
- import { S as SchemaDeclaration } from '../plugin-CHd2ZiGC.cjs';
1
+ import { D as DatabaseAdapter } from '../adapter-BJ8Iejvs.cjs';
2
+ import { S as SchemaDeclaration } from '../plugin-Wwp1nvo0.cjs';
3
3
  import { S as Signals } from '../signals-kPmOlKDX.cjs';
4
4
 
5
5
  type Row = Record<string, unknown>;
@@ -1,5 +1,5 @@
1
- import { D as DatabaseAdapter } from '../adapter-D3lTy-4E.js';
2
- import { S as SchemaDeclaration } from '../plugin-Cg9usuUi.js';
1
+ import { D as DatabaseAdapter } from '../adapter-BJ8Iejvs.js';
2
+ import { S as SchemaDeclaration } from '../plugin-CSLKKgXT.js';
3
3
  import { S as Signals } from '../signals-kPmOlKDX.js';
4
4
 
5
5
  type Row = Record<string, unknown>;
@@ -1,7 +1,7 @@
1
1
  import { S as SqlAdapterOptions, a as SqlExec } from '../adapter-B7R4rXtJ.cjs';
2
2
  export { b as SqliteResult } from '../adapter-B7R4rXtJ.cjs';
3
- import { D as DatabaseAdapter } from '../adapter-D3lTy-4E.cjs';
4
- import { S as SchemaDeclaration } from '../plugin-CHd2ZiGC.cjs';
3
+ import { D as DatabaseAdapter } from '../adapter-BJ8Iejvs.cjs';
4
+ import { S as SchemaDeclaration } from '../plugin-Wwp1nvo0.cjs';
5
5
 
6
6
  type SqliteAdapterOptions = SqlAdapterOptions;
7
7
  /**
@@ -1,7 +1,7 @@
1
1
  import { S as SqlAdapterOptions, a as SqlExec } from '../adapter-B7R4rXtJ.js';
2
2
  export { b as SqliteResult } from '../adapter-B7R4rXtJ.js';
3
- import { D as DatabaseAdapter } from '../adapter-D3lTy-4E.js';
4
- import { S as SchemaDeclaration } from '../plugin-Cg9usuUi.js';
3
+ import { D as DatabaseAdapter } from '../adapter-BJ8Iejvs.js';
4
+ import { S as SchemaDeclaration } from '../plugin-CSLKKgXT.js';
5
5
 
6
6
  type SqliteAdapterOptions = SqlAdapterOptions;
7
7
  /**
@@ -0,0 +1,13 @@
1
+ // src/core/html.ts
2
+ var ESCAPES = {
3
+ "&": "&amp;",
4
+ "<": "&lt;",
5
+ ">": "&gt;",
6
+ '"': "&quot;",
7
+ "'": "&#39;"
8
+ };
9
+ var escapeHtml = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ESCAPES[char] ?? char);
10
+
11
+ export { escapeHtml };
12
+ //# sourceMappingURL=chunk-R3BE67L7.js.map
13
+ //# sourceMappingURL=chunk-R3BE67L7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/html.ts"],"names":[],"mappings":";AAAA,IAAM,OAAA,GAAkC;AAAA,EACtC,GAAA,EAAK,OAAA;AAAA,EACL,GAAA,EAAK,MAAA;AAAA,EACL,GAAA,EAAK,MAAA;AAAA,EACL,GAAA,EAAK,QAAA;AAAA,EACL,GAAA,EAAK;AACP,CAAA;AAGO,IAAM,UAAA,GAAa,CAAC,KAAA,KACzB,MAAA,CAAO,SAAS,EAAE,CAAA,CAAE,OAAA,CAAQ,UAAA,EAAY,CAAC,IAAA,KAAS,OAAA,CAAQ,IAAI,KAAK,IAAI","file":"chunk-R3BE67L7.js","sourcesContent":["const ESCAPES: Record<string, string> = {\n \"&\": \"&amp;\",\n \"<\": \"&lt;\",\n \">\": \"&gt;\",\n '\"': \"&quot;\",\n \"'\": \"&#39;\",\n};\n\n/** For template literals: anything a user typed goes through this, or their HTML ships from your domain. */\nexport const escapeHtml = (value: unknown): string =>\n String(value ?? \"\").replace(/[&<>\"']/g, (char) => ESCAPES[char] ?? char);\n"]}
@@ -0,0 +1,100 @@
1
+ // src/providers/telegram/index.ts
2
+ var telegramStartLink = (botUsername, code) => `https://t.me/${botUsername.replace(/^@/, "")}?start=${encodeURIComponent(code)}`;
3
+ function telegramBot(options) {
4
+ const doFetch = options.fetch ?? globalThis.fetch;
5
+ const base = `${(options.apiBase ?? "https://api.telegram.org").replace(/\/$/, "")}/bot${options.token}`;
6
+ const timeoutMs = options.timeoutMs ?? 1e4;
7
+ const redact = (text) => text.split(options.token).join("<token>");
8
+ async function call(method, body, signal, budgetMs = timeoutMs) {
9
+ const timeout = AbortSignal.timeout(budgetMs);
10
+ const merged = signal ? AbortSignal.any([signal, timeout]) : timeout;
11
+ const response = await doFetch(`${base}/${method}`, {
12
+ method: "POST",
13
+ headers: { "content-type": "application/json" },
14
+ body: JSON.stringify(body),
15
+ signal: merged
16
+ });
17
+ const data = await response.json().catch(() => ({ ok: false }));
18
+ return { status: response.status, data };
19
+ }
20
+ async function must(method, body) {
21
+ const { data } = await call(method, body);
22
+ if (!data.ok)
23
+ throw new Error(redact(`telegram ${method} failed: ${data.description ?? "unknown"}`));
24
+ return data.result;
25
+ }
26
+ return {
27
+ name: "telegram",
28
+ async send(message) {
29
+ const body = {
30
+ chat_id: message.chatId,
31
+ text: message.text,
32
+ parse_mode: "HTML",
33
+ link_preview_options: { is_disabled: true }
34
+ };
35
+ if (message.buttons?.length) {
36
+ body.reply_markup = {
37
+ inline_keyboard: [
38
+ message.buttons.map((button) => ({ text: button.text, url: button.url }))
39
+ ]
40
+ };
41
+ }
42
+ let status;
43
+ let data;
44
+ try {
45
+ ({ status, data } = await call(
46
+ "sendMessage",
47
+ body,
48
+ message.signal
49
+ ));
50
+ } catch (error) {
51
+ const text = error instanceof Error ? error.message : String(error);
52
+ return { ok: false, error: redact(text), retryable: true, gone: false };
53
+ }
54
+ if (data.ok) return { ok: true, messageId: data.result.message_id };
55
+ const description = redact(data.description ?? `HTTP ${status}`);
56
+ const code = data.error_code ?? status;
57
+ if (code === 429) {
58
+ return {
59
+ ok: false,
60
+ error: description,
61
+ retryable: true,
62
+ gone: false,
63
+ retryAfterSeconds: data.parameters?.retry_after
64
+ };
65
+ }
66
+ const gone = code === 403 || code === 400 && /chat not found|user is deactivated|bot was kicked/i.test(description);
67
+ return { ok: false, error: description, retryable: code >= 500, gone };
68
+ },
69
+ async getUpdates({ offset, timeoutSeconds = 25, signal }) {
70
+ const { data } = await call(
71
+ "getUpdates",
72
+ {
73
+ ...offset === void 0 ? {} : { offset },
74
+ timeout: timeoutSeconds,
75
+ allowed_updates: ["message", "my_chat_member"]
76
+ },
77
+ signal,
78
+ (timeoutSeconds + 10) * 1e3
79
+ );
80
+ if (!data.ok)
81
+ throw new Error(redact(`telegram getUpdates failed: ${data.description ?? "unknown"}`));
82
+ return data.result;
83
+ },
84
+ setWebhook: async (url, secretToken) => {
85
+ await must("setWebhook", {
86
+ url,
87
+ secret_token: secretToken,
88
+ allowed_updates: ["message", "my_chat_member"]
89
+ });
90
+ },
91
+ deleteWebhook: async () => {
92
+ await must("deleteWebhook", {});
93
+ },
94
+ getMe: () => must("getMe", {})
95
+ };
96
+ }
97
+
98
+ export { telegramBot, telegramStartLink };
99
+ //# sourceMappingURL=chunk-SSHVZHSJ.js.map
100
+ //# sourceMappingURL=chunk-SSHVZHSJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/providers/telegram/index.ts"],"names":[],"mappings":"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","file":"chunk-SSHVZHSJ.js","sourcesContent":["export type TelegramButton = { text: string; url: string };\n\nexport type TelegramMessage = {\n chatId: string;\n /** Telegram HTML: <b>, <i>, <a href>, <code>, <pre>. Escape user text with escapeHtml(). */\n text: string;\n buttons?: readonly TelegramButton[] | undefined;\n signal?: AbortSignal | undefined;\n};\n\nexport type TelegramSendResult =\n | { ok: true; messageId: number }\n | {\n ok: false;\n error: string;\n /** Worth another attempt: rate limit, 5xx, network. */\n retryable: boolean;\n /** The chat can never receive again (user blocked the bot, chat deleted). Prune it. */\n gone: boolean;\n retryAfterSeconds?: number | undefined;\n };\n\n/** The slice of a Bot API update the plugin reads. Everything else passes through untouched. */\nexport type TelegramUpdate = {\n update_id: number;\n message?: {\n text?: string;\n chat: { id: number | string; type: string; username?: string; title?: string };\n from?: { id: number; username?: string; first_name?: string };\n };\n my_chat_member?: {\n chat: { id: number | string; type: string; username?: string; title?: string };\n new_chat_member: { status: string };\n };\n};\n\nexport type TelegramProvider = {\n name: string;\n send(message: TelegramMessage): Promise<TelegramSendResult>;\n /** Long-polls for updates. For development or hosts without a public URL. */\n getUpdates(options: {\n offset?: number | undefined;\n timeoutSeconds?: number | undefined;\n signal?: AbortSignal | undefined;\n }): Promise<TelegramUpdate[]>;\n /** Points Telegram at your webhook route. Run once per deployment URL. */\n setWebhook(url: string, secretToken: string): Promise<void>;\n deleteWebhook(): Promise<void>;\n getMe(): Promise<{ id: number; username: string }>;\n};\n\nexport type TelegramBotOptions = {\n /** From @BotFather. Never appears in errors or logs. */\n token: string;\n fetch?: typeof globalThis.fetch | undefined;\n /** Default https://api.telegram.org. A local fake for tests. */\n apiBase?: string | undefined;\n timeoutMs?: number | undefined;\n};\n\ntype ApiResponse<T> =\n | { ok: true; result: T }\n | { ok: false; description?: string; error_code?: number; parameters?: { retry_after?: number } };\n\n/** Deep link that opens the bot with a start parameter. Codes are [A-Za-z0-9_-], at most 64 chars. */\nexport const telegramStartLink = (botUsername: string, code: string) =>\n `https://t.me/${botUsername.replace(/^@/, \"\")}?start=${encodeURIComponent(code)}`;\n\n/** Bot API over fetch, so it runs on edge runtimes. Failures are classified, never thrown, on send. */\nexport function telegramBot(options: TelegramBotOptions): TelegramProvider {\n const doFetch = options.fetch ?? globalThis.fetch;\n const base = `${(options.apiBase ?? \"https://api.telegram.org\").replace(/\\/$/, \"\")}/bot${options.token}`;\n const timeoutMs = options.timeoutMs ?? 10_000;\n const redact = (text: string) => text.split(options.token).join(\"<token>\");\n\n async function call<T>(\n method: string,\n body: Record<string, unknown>,\n signal?: AbortSignal,\n budgetMs = timeoutMs,\n ): Promise<{ status: number; data: ApiResponse<T> }> {\n const timeout = AbortSignal.timeout(budgetMs);\n const merged = signal ? AbortSignal.any([signal, timeout]) : timeout;\n const response = await doFetch(`${base}/${method}`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: merged,\n });\n const data = (await response.json().catch(() => ({ ok: false }))) as ApiResponse<T>;\n return { status: response.status, data };\n }\n\n async function must<T>(method: string, body: Record<string, unknown>): Promise<T> {\n const { data } = await call<T>(method, body);\n if (!data.ok)\n throw new Error(redact(`telegram ${method} failed: ${data.description ?? \"unknown\"}`));\n return data.result;\n }\n\n return {\n name: \"telegram\",\n\n async send(message) {\n const body: Record<string, unknown> = {\n chat_id: message.chatId,\n text: message.text,\n parse_mode: \"HTML\",\n link_preview_options: { is_disabled: true },\n };\n if (message.buttons?.length) {\n body.reply_markup = {\n inline_keyboard: [\n message.buttons.map((button) => ({ text: button.text, url: button.url })),\n ],\n };\n }\n\n let status: number;\n let data: ApiResponse<{ message_id: number }>;\n try {\n ({ status, data } = await call<{ message_id: number }>(\n \"sendMessage\",\n body,\n message.signal,\n ));\n } catch (error) {\n const text = error instanceof Error ? error.message : String(error);\n return { ok: false, error: redact(text), retryable: true, gone: false };\n }\n\n if (data.ok) return { ok: true, messageId: data.result.message_id };\n\n const description = redact(data.description ?? `HTTP ${status}`);\n const code = data.error_code ?? status;\n if (code === 429) {\n return {\n ok: false,\n error: description,\n retryable: true,\n gone: false,\n retryAfterSeconds: data.parameters?.retry_after,\n };\n }\n // 403: the user blocked the bot or was deactivated. 400 \"chat not found\": deleted or never existed.\n const gone =\n code === 403 ||\n (code === 400 && /chat not found|user is deactivated|bot was kicked/i.test(description));\n return { ok: false, error: description, retryable: code >= 500, gone };\n },\n\n async getUpdates({ offset, timeoutSeconds = 25, signal }) {\n // Telegram holds the request open for `timeout` seconds; the budget must outlive it.\n const { data } = await call<TelegramUpdate[]>(\n \"getUpdates\",\n {\n ...(offset === undefined ? {} : { offset }),\n timeout: timeoutSeconds,\n allowed_updates: [\"message\", \"my_chat_member\"],\n },\n signal,\n (timeoutSeconds + 10) * 1000,\n );\n if (!data.ok)\n throw new Error(redact(`telegram getUpdates failed: ${data.description ?? \"unknown\"}`));\n return data.result;\n },\n\n setWebhook: async (url, secretToken) => {\n await must(\"setWebhook\", {\n url,\n secret_token: secretToken,\n allowed_updates: [\"message\", \"my_chat_member\"],\n });\n },\n deleteWebhook: async () => {\n await must(\"deleteWebhook\", {});\n },\n getMe: () => must(\"getMe\", {}),\n };\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -1,8 +1,8 @@
1
- import { C as Channel, b as DeliveryMode, S as SkipReason, c as DeliveryRecord, D as DatabaseAdapter, R as Recipient } from './adapter-D3lTy-4E.cjs';
2
- export { d as ClaimArgs, e as ClaimedDelivery, f as DeliveryOutcome, g as DeliveryRelease, a as DeliveryStatus, h as FeedPage, i as FeedQuery, F as Frequency, I as InsertDelivery, j as InsertNotification, N as NotificationRecord, P as PreferenceRecord } from './adapter-D3lTy-4E.cjs';
1
+ import { C as Channel, b as DeliveryMode, S as SkipReason, c as DeliveryRecord, D as DatabaseAdapter, R as Recipient } from './adapter-BJ8Iejvs.cjs';
2
+ export { d as ClaimArgs, e as ClaimedDelivery, f as DeliveryOutcome, g as DeliveryRelease, a as DeliveryStatus, h as FeedPage, i as FeedQuery, F as Frequency, I as InsertDelivery, j as InsertNotification, N as NotificationRecord, P as PreferenceRecord } from './adapter-BJ8Iejvs.cjs';
3
3
  import { StandardSchemaV1 } from '@standard-schema/spec';
4
- import { R as RouteScope, A as AnyPlugin, P as Promisable, L as Logger } from './plugin-CHd2ZiGC.cjs';
5
- export { a as ActionsOf, b as AfterDeliverContext, c as AnyClientPlugin, B as BeforeSendContext, d as BeforeSendResult, C as ChannelDecision, e as ClientFetch, f as ClientPlugin, g as ConfigError, D as DEFAULT_TOKEN_TTL_SECONDS, E as EasyPingError, h as EasyPingPlugin, F as FieldDeclaration, i as FieldType, M as MAX_TOKEN_TTL_SECONDS, j as PluginHooks, k as PrepareContext, l as ResolveChannelsContext, m as RouteContext, n as RouteDefinition, S as SchemaDeclaration, o as SignRequest, T as TableDeclaration, p as TokenClaims, U as UnionToIntersection, V as ValidationError, q as consoleLogger, r as defineClientPlugin, s as definePlugin, t as expiresIn, u as signToken, v as verifyToken } from './plugin-CHd2ZiGC.cjs';
4
+ import { R as RouteScope, A as AnyPlugin, P as Promisable, L as Logger } from './plugin-Wwp1nvo0.cjs';
5
+ export { a as ActionsOf, b as AfterDeliverContext, c as AnyClientPlugin, B as BeforeSendContext, d as BeforeSendResult, C as ChannelDecision, e as ClientFetch, f as ClientPlugin, g as ConfigError, D as DEFAULT_TOKEN_TTL_SECONDS, E as EasyPingError, h as EasyPingPlugin, F as FieldDeclaration, i as FieldType, M as MAX_TOKEN_TTL_SECONDS, j as PluginHooks, k as PrepareContext, l as ResolveChannelsContext, m as RouteContext, n as RouteDefinition, S as SchemaDeclaration, o as SignRequest, T as TableDeclaration, p as TokenClaims, U as UnionToIntersection, V as ValidationError, q as consoleLogger, r as defineClientPlugin, s as definePlugin, t as expiresIn, u as signToken, v as verifyToken } from './plugin-Wwp1nvo0.cjs';
6
6
  import { E as EmailProvider } from './provider-CA45WVqt.cjs';
7
7
  export { a as EmailMessage, P as ProviderSendResult } from './provider-CA45WVqt.cjs';
8
8
  import { S as Signals } from './signals-kPmOlKDX.cjs';
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { C as Channel, b as DeliveryMode, S as SkipReason, c as DeliveryRecord, D as DatabaseAdapter, R as Recipient } from './adapter-D3lTy-4E.js';
2
- export { d as ClaimArgs, e as ClaimedDelivery, f as DeliveryOutcome, g as DeliveryRelease, a as DeliveryStatus, h as FeedPage, i as FeedQuery, F as Frequency, I as InsertDelivery, j as InsertNotification, N as NotificationRecord, P as PreferenceRecord } from './adapter-D3lTy-4E.js';
1
+ import { C as Channel, b as DeliveryMode, S as SkipReason, c as DeliveryRecord, D as DatabaseAdapter, R as Recipient } from './adapter-BJ8Iejvs.js';
2
+ export { d as ClaimArgs, e as ClaimedDelivery, f as DeliveryOutcome, g as DeliveryRelease, a as DeliveryStatus, h as FeedPage, i as FeedQuery, F as Frequency, I as InsertDelivery, j as InsertNotification, N as NotificationRecord, P as PreferenceRecord } from './adapter-BJ8Iejvs.js';
3
3
  import { StandardSchemaV1 } from '@standard-schema/spec';
4
- import { R as RouteScope, A as AnyPlugin, P as Promisable, L as Logger } from './plugin-Cg9usuUi.js';
5
- export { a as ActionsOf, b as AfterDeliverContext, c as AnyClientPlugin, B as BeforeSendContext, d as BeforeSendResult, C as ChannelDecision, e as ClientFetch, f as ClientPlugin, g as ConfigError, D as DEFAULT_TOKEN_TTL_SECONDS, E as EasyPingError, h as EasyPingPlugin, F as FieldDeclaration, i as FieldType, M as MAX_TOKEN_TTL_SECONDS, j as PluginHooks, k as PrepareContext, l as ResolveChannelsContext, m as RouteContext, n as RouteDefinition, S as SchemaDeclaration, o as SignRequest, T as TableDeclaration, p as TokenClaims, U as UnionToIntersection, V as ValidationError, q as consoleLogger, r as defineClientPlugin, s as definePlugin, t as expiresIn, u as signToken, v as verifyToken } from './plugin-Cg9usuUi.js';
4
+ import { R as RouteScope, A as AnyPlugin, P as Promisable, L as Logger } from './plugin-CSLKKgXT.js';
5
+ export { a as ActionsOf, b as AfterDeliverContext, c as AnyClientPlugin, B as BeforeSendContext, d as BeforeSendResult, C as ChannelDecision, e as ClientFetch, f as ClientPlugin, g as ConfigError, D as DEFAULT_TOKEN_TTL_SECONDS, E as EasyPingError, h as EasyPingPlugin, F as FieldDeclaration, i as FieldType, M as MAX_TOKEN_TTL_SECONDS, j as PluginHooks, k as PrepareContext, l as ResolveChannelsContext, m as RouteContext, n as RouteDefinition, S as SchemaDeclaration, o as SignRequest, T as TableDeclaration, p as TokenClaims, U as UnionToIntersection, V as ValidationError, q as consoleLogger, r as defineClientPlugin, s as definePlugin, t as expiresIn, u as signToken, v as verifyToken } from './plugin-CSLKKgXT.js';
6
6
  import { E as EmailProvider } from './provider-CA45WVqt.js';
7
7
  export { a as EmailMessage, P as ProviderSendResult } from './provider-CA45WVqt.js';
8
8
  import { S as Signals } from './signals-kPmOlKDX.js';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createRunner } from './chunk-JKTMUMQK.js';
2
2
  export { createRunner, redactErrorMessage } from './chunk-JKTMUMQK.js';
3
+ export { escapeHtml } from './chunk-R3BE67L7.js';
3
4
  export { defineClientPlugin, definePlugin } from './chunk-E4IZ2CQC.js';
4
5
  import { createHandler, createScopedSigner } from './chunk-JOVIZXO6.js';
5
6
  export { DEFAULT_TOKEN_TTL_SECONDS, MAX_BODY_BYTES, MAX_TOKEN_TTL_SECONDS, expiresIn, readJsonBody, signToken, verifyToken } from './chunk-JOVIZXO6.js';
@@ -18,16 +19,6 @@ function defineNotification(definition) {
18
19
  return definition;
19
20
  }
20
21
 
21
- // src/core/html.ts
22
- var ESCAPES = {
23
- "&": "&amp;",
24
- "<": "&lt;",
25
- ">": "&gt;",
26
- '"': "&quot;",
27
- "'": "&#39;"
28
- };
29
- var escapeHtml = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ESCAPES[char] ?? char);
30
-
31
22
  // src/core/rate-limit.ts
32
23
  var clientAddress = (request) => {
33
24
  const headers = request.headers;
@@ -535,6 +526,6 @@ function easyPing(config) {
535
526
  };
536
527
  }
537
528
 
538
- export { INBOX_VERSION_HEADER, MIN_SECRET_LENGTH, createRateLimiter, defineNotification, easyPing, escapeHtml };
529
+ export { INBOX_VERSION_HEADER, MIN_SECRET_LENGTH, createRateLimiter, defineNotification, easyPing };
539
530
  //# sourceMappingURL=index.js.map
540
531
  //# sourceMappingURL=index.js.map