@galhardoalex/telegram-logger 0.0.0-stage โ†’ 0.1.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 Alex Galhardo
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 CHANGED
@@ -1,3 +1,204 @@
1
- # Temporary Holding Version
1
+ # telegram-logger
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![JSR](https://jsr.io/badges/@galhardoalex/telegram-logger)](https://jsr.io/@galhardoalex/telegram-logger)
4
+ [![JSR Score](https://jsr.io/badges/@galhardoalex/telegram-logger/score)](https://jsr.io/@galhardoalex/telegram-logger)
5
+ [![npm](https://img.shields.io/npm/v/@galhardoalex/telegram-logger)](https://www.npmjs.com/package/@galhardoalex/telegram-logger)
6
+ [![CI](https://github.com/AlexGalhardo/telegram-logger/actions/workflows/ci.yml/badge.svg)](https://github.com/AlexGalhardo/telegram-logger/actions/workflows/ci.yml)
7
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
+
9
+ Send errors, warnings, events and contact-form submissions to your **Telegram** chats and channels.
10
+
11
+ - **Zero dependencies**: uses the native `fetch`.
12
+ - **Runs anywhere**: Node.js 18+, Bun, Deno, Next.js (Node and Edge), Cloudflare Workers.
13
+ - **Never throws**: a failed delivery never crashes your app or your `catch` block.
14
+ - **Fully typed**: written in TypeScript, ships ESM, CommonJS and type declarations.
15
+ - **Readable messages**: emoji levels, app name, environment, metadata, stack traces and causes.
16
+ - **Safe**: user content is HTML-escaped and every message is kept under Telegram's 4096-character limit.
17
+
18
+ ```text
19
+ ๐Ÿ”ด ERROR ยท my-site ยท production
20
+ TypeError: Cannot read properties of undefined (reading 'id')
21
+
22
+ route: /api/checkout
23
+ userId: 42
24
+
25
+ at POST (app/api/checkout/route.ts:12:20)
26
+ ...
27
+ ๐Ÿ•’ 2026-10-15T14:40:00.000Z
28
+ ```
29
+
30
+ ## Install
31
+
32
+ **npm**
33
+
34
+ ```bash
35
+ npm install @galhardoalex/telegram-logger
36
+ ```
37
+
38
+ **pnpm**
39
+
40
+ ```bash
41
+ pnpm add @galhardoalex/telegram-logger
42
+ ```
43
+
44
+ **yarn**
45
+
46
+ ```bash
47
+ yarn add @galhardoalex/telegram-logger
48
+ ```
49
+
50
+ **bun**
51
+
52
+ ```bash
53
+ bun add @galhardoalex/telegram-logger
54
+ ```
55
+
56
+ **deno**
57
+
58
+ ```bash
59
+ deno add jsr:@galhardoalex/telegram-logger
60
+ ```
61
+
62
+ **From JSR, with any package manager**
63
+
64
+ ```bash
65
+ npx jsr add @galhardoalex/telegram-logger
66
+ ```
67
+
68
+ ## Get a bot token and a chat id
69
+
70
+ 1. Open [@BotFather](https://t.me/BotFather), send `/newbot`, and copy the **token**.
71
+ 2. Add the bot to your channel or group. In a channel, make it an **admin** so it can post.
72
+ 3. Send any message in that chat, then open `https://api.telegram.org/bot<TOKEN>/getUpdates` and copy `chat.id`.
73
+ Channel and supergroup ids start with `-100`.
74
+ 4. Put both values in your environment:
75
+
76
+ ```bash
77
+ TELEGRAM_BOT_TOKEN=123456789:AAH...
78
+ TELEGRAM_CHAT_ID=-1001234567890
79
+ ```
80
+
81
+ > โš ๏ธ Keep the token on the **server**. Never expose it to the browser (no `NEXT_PUBLIC_` prefix).
82
+
83
+ ## Usage
84
+
85
+ ```ts
86
+ import { createTelegramLogger } from "@galhardoalex/telegram-logger";
87
+
88
+ export const logger = createTelegramLogger({
89
+ botToken: process.env.TELEGRAM_BOT_TOKEN, // optional: falls back to TELEGRAM_BOT_TOKEN
90
+ chatId: process.env.TELEGRAM_CHAT_ID, // optional: falls back to TELEGRAM_CHAT_ID
91
+ appName: "my-site",
92
+ environment: process.env.NODE_ENV,
93
+ });
94
+
95
+ // Errors: pass whatever you caught. Error objects include their stack and cause.
96
+ try {
97
+ await chargeCustomer();
98
+ } catch (err) {
99
+ await logger.error(err, { route: "/api/checkout", userId: 42 });
100
+ }
101
+
102
+ // Other levels
103
+ await logger.debug("Cache miss", { key: "home" });
104
+ await logger.info("Cron job finished", { durationMs: 1234 });
105
+ await logger.success("New subscription ๐ŸŽ‰", { plan: "pro" });
106
+ await logger.warn("404 Not Found", { path: "/old-page" });
107
+ await logger.fatal(new Error("Database unreachable"));
108
+
109
+ // Contact forms
110
+ await logger.contact({
111
+ name: "Ana",
112
+ email: "ana@example.com",
113
+ phone: "+55 11 99999-9999",
114
+ message: "I'd like a quote for a new website.",
115
+ budget: "R$ 5.000", // any extra field is listed too
116
+ });
117
+ ```
118
+
119
+ > In serverless functions (Vercel, Netlify, AWS Lambda...) always **`await`** the call. Otherwise the function may be frozen before the message is sent.
120
+
121
+ ## Different chats per message type
122
+
123
+ ```ts
124
+ const logger = createTelegramLogger({
125
+ chatId: "-100111", // default chat
126
+ chatIds: {
127
+ error: "-100222", // errors and fatals go to #alerts
128
+ fatal: "-100222",
129
+ contact: "-100333", // leads go to #leads
130
+ },
131
+ });
132
+
133
+ await logger.info("Goes to -100111");
134
+ await logger.error("Goes to -100222");
135
+ await logger.warn("Goes to -100444", {}, { chatId: "-100444" }); // per-call override
136
+ ```
137
+
138
+ ## Options
139
+
140
+ | Option | Type | Default | Description |
141
+ | ---------------------- | ---------------------------------- | -------------------------- | ------------------------------------------------------------------------------------ |
142
+ | `botToken` | `string` | `env.TELEGRAM_BOT_TOKEN` | Bot token from @BotFather. |
143
+ | `chatId` | `string \| number` | `env.TELEGRAM_CHAT_ID` | Default chat. |
144
+ | `chatIds` | `{ [kind]: chatId }` | `{}` | Per-kind chats (`debug`, `info`, `success`, `warn`, `error`, `fatal`, `contact`). |
145
+ | `appName` | `string` | none | Shown in the message header. |
146
+ | `environment` | `string` | none | Shown in the message header. |
147
+ | `enabled` | `boolean` | `true` | `false` turns every method into a no-op. |
148
+ | `minLevel` | `LogLevel` | `"debug"` | Skip lower levels. `contact` is never skipped. |
149
+ | `timeoutMs` | `number` | `5000` | Timeout of each request to Telegram. |
150
+ | `maxRetryAfterSeconds` | `number` | `5` | On HTTP 429, wait `retry_after` and retry once if it is at most this. `0` disables. |
151
+ | `disableNotification` | `boolean` | `false` | Send silently. |
152
+ | `messageThreadId` | `number` | none | Topic id in forum supergroups. |
153
+ | `onError` | `(error: TelegramLoggerError) => void` | none | Called when a message fails to send. |
154
+ | `fetch` | `(url, init) => Promise<Response>` | `globalThis.fetch` | Custom fetch (proxies, tests). |
155
+ | `apiBaseUrl` | `string` | `https://api.telegram.org` | Custom Bot API server. |
156
+
157
+ Every method also accepts per-call options as its last argument: `{ chatId, title, disableNotification, messageThreadId }`.
158
+
159
+ ## Methods
160
+
161
+ | Method | Description |
162
+ | --------------------------------------------- | --------------------------------------------------- |
163
+ | `debug` / `info` / `success` / `warn` / `error` / `fatal` `(input, meta?, options?)` | Log at that level. `input` can be an `Error`, a string or any value. |
164
+ | `log(level, input, meta?, options?)` | Log with a dynamic level. |
165
+ | `contact(data, options?)` | Format a contact-form submission. |
166
+ | `send(html, options?)` | Send your own [Telegram HTML](https://core.telegram.org/bots/api#html-style) as is. Escape user input with `escapeHtml`. |
167
+
168
+ All methods return `Promise<SendResult>` and **never reject**:
169
+
170
+ ```ts
171
+ const result = await logger.error(err);
172
+ if (!result.ok) {
173
+ // result.reason: "disabled" | "below_min_level" | "missing_config" | "telegram_error" | "network_error" | "timeout"
174
+ console.warn("Telegram delivery failed:", result.reason, result.error?.message);
175
+ }
176
+ ```
177
+
178
+ The formatters are exported too (`formatLog`, `formatContact`, `escapeHtml`), in case you want to build messages yourself.
179
+
180
+ ## Recipes
181
+
182
+ - **Disable in development:** `enabled: process.env.NODE_ENV === "production"`.
183
+ - **Only important stuff:** `minLevel: "warn"`.
184
+ - **Custom title:** `logger.error(err, meta, { title: "๐Ÿ’ณ Checkout failed" })`.
185
+
186
+ ## Documentation
187
+
188
+ | Guide | ๐Ÿ‡บ๐Ÿ‡ธ English | ๐Ÿ‡ง๐Ÿ‡ท Portuguรชs |
189
+ | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
190
+ | **Usage guide**: Next.js, React, Express, Hono, Workers, Deno... | [docs/en/setup.md](https://github.com/AlexGalhardo/telegram-logger/blob/main/docs/en/setup.md) | [docs/pt-br/setup.md](https://github.com/AlexGalhardo/telegram-logger/blob/main/docs/pt-br/setup.md) |
191
+ | **Releasing**: publishing and updating on npm and JSR | [docs/en/how-to-deploy-and-update-this-package.md](https://github.com/AlexGalhardo/telegram-logger/blob/main/docs/en/how-to-deploy-and-update-this-package.md) | [docs/pt-br/how-to-deploy-and-update-this-package.md](https://github.com/AlexGalhardo/telegram-logger/blob/main/docs/pt-br/how-to-deploy-and-update-this-package.md) |
192
+
193
+ ## Contributing
194
+
195
+ ```bash
196
+ bun install
197
+ bun run check # lint + typecheck + tests + build + package validation
198
+ ```
199
+
200
+ See [CONTRIBUTING.md](https://github.com/AlexGalhardo/telegram-logger/blob/main/CONTRIBUTING.md). To report a vulnerability, see [SECURITY.md](https://github.com/AlexGalhardo/telegram-logger/blob/main/SECURITY.md).
201
+
202
+ ## License
203
+
204
+ [MIT](LICENSE) ยฉ Alex Galhardo
@@ -0,0 +1,39 @@
1
+ import type { ContactData, LogMeta, MessageKind } from "./types.cjs";
2
+ /** Telegram rejects messages longer than this (after entity parsing). */
3
+ export declare const MAX_MESSAGE_LENGTH = 4096;
4
+ /**
5
+ * Escape text for Telegram's HTML parse mode.
6
+ *
7
+ * @param text Untrusted text.
8
+ * @returns The text with `&`, `<` and `>` escaped.
9
+ */
10
+ export declare function escapeHtml(text: string): string;
11
+ /** Options used by the formatters. */
12
+ export interface FormatContext {
13
+ /** App name shown in the header. */
14
+ appName?: string;
15
+ /** Environment shown in the header. */
16
+ environment?: string;
17
+ /** Replaces the default title (first bold line). */
18
+ title?: string;
19
+ /** Clock used for the timestamp. Defaults to `new Date()`. */
20
+ now?: Date;
21
+ }
22
+ /**
23
+ * Build the Telegram-HTML text for a log message.
24
+ *
25
+ * @param kind Message kind (log level).
26
+ * @param input The value being logged: an `Error`, a string or anything JSON-serializable.
27
+ * @param meta Extra key/value context.
28
+ * @param ctx App name, environment, title override and clock.
29
+ * @returns Text ready to send with `parse_mode: "HTML"`, at most 4096 characters long.
30
+ */
31
+ export declare function formatLog(kind: MessageKind, input: unknown, meta?: LogMeta, ctx?: FormatContext): string;
32
+ /**
33
+ * Build the Telegram-HTML text for a contact form submission.
34
+ *
35
+ * @param data Form fields.
36
+ * @param ctx App name, environment, title override and clock.
37
+ * @returns Text ready to send with `parse_mode: "HTML"`, at most 4096 characters long.
38
+ */
39
+ export declare function formatContact(data: ContactData, ctx?: FormatContext): string;
@@ -0,0 +1,39 @@
1
+ import type { ContactData, LogMeta, MessageKind } from "./types.js";
2
+ /** Telegram rejects messages longer than this (after entity parsing). */
3
+ export declare const MAX_MESSAGE_LENGTH = 4096;
4
+ /**
5
+ * Escape text for Telegram's HTML parse mode.
6
+ *
7
+ * @param text Untrusted text.
8
+ * @returns The text with `&`, `<` and `>` escaped.
9
+ */
10
+ export declare function escapeHtml(text: string): string;
11
+ /** Options used by the formatters. */
12
+ export interface FormatContext {
13
+ /** App name shown in the header. */
14
+ appName?: string;
15
+ /** Environment shown in the header. */
16
+ environment?: string;
17
+ /** Replaces the default title (first bold line). */
18
+ title?: string;
19
+ /** Clock used for the timestamp. Defaults to `new Date()`. */
20
+ now?: Date;
21
+ }
22
+ /**
23
+ * Build the Telegram-HTML text for a log message.
24
+ *
25
+ * @param kind Message kind (log level).
26
+ * @param input The value being logged: an `Error`, a string or anything JSON-serializable.
27
+ * @param meta Extra key/value context.
28
+ * @param ctx App name, environment, title override and clock.
29
+ * @returns Text ready to send with `parse_mode: "HTML"`, at most 4096 characters long.
30
+ */
31
+ export declare function formatLog(kind: MessageKind, input: unknown, meta?: LogMeta, ctx?: FormatContext): string;
32
+ /**
33
+ * Build the Telegram-HTML text for a contact form submission.
34
+ *
35
+ * @param data Form fields.
36
+ * @param ctx App name, environment, title override and clock.
37
+ * @returns Text ready to send with `parse_mode: "HTML"`, at most 4096 characters long.
38
+ */
39
+ export declare function formatContact(data: ContactData, ctx?: FormatContext): string;
@@ -0,0 +1,29 @@
1
+ import { type TelegramLogger, type TelegramLoggerOptions } from "./types.cjs";
2
+ /**
3
+ * Create a Telegram logger.
4
+ *
5
+ * Every method returns a promise that always resolves (never rejects) with a {@linkcode SendResult}.
6
+ * In serverless functions, `await` the call so the request finishes before the function is frozen.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { createTelegramLogger } from "@galhardoalex/telegram-logger";
11
+ *
12
+ * const logger = createTelegramLogger({
13
+ * botToken: process.env.TELEGRAM_BOT_TOKEN,
14
+ * chatId: process.env.TELEGRAM_CHAT_ID,
15
+ * appName: "my-site",
16
+ * });
17
+ *
18
+ * try {
19
+ * await checkout();
20
+ * } catch (err) {
21
+ * await logger.error(err, { route: "/api/checkout" });
22
+ * }
23
+ * ```
24
+ *
25
+ * @param options Logger configuration. `botToken` and `chatId` fall back to the
26
+ * `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID` environment variables.
27
+ * @returns A {@linkcode TelegramLogger}.
28
+ */
29
+ export declare function createTelegramLogger(options?: TelegramLoggerOptions): TelegramLogger;
@@ -0,0 +1,29 @@
1
+ import { type TelegramLogger, type TelegramLoggerOptions } from "./types.js";
2
+ /**
3
+ * Create a Telegram logger.
4
+ *
5
+ * Every method returns a promise that always resolves (never rejects) with a {@linkcode SendResult}.
6
+ * In serverless functions, `await` the call so the request finishes before the function is frozen.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { createTelegramLogger } from "@galhardoalex/telegram-logger";
11
+ *
12
+ * const logger = createTelegramLogger({
13
+ * botToken: process.env.TELEGRAM_BOT_TOKEN,
14
+ * chatId: process.env.TELEGRAM_CHAT_ID,
15
+ * appName: "my-site",
16
+ * });
17
+ *
18
+ * try {
19
+ * await checkout();
20
+ * } catch (err) {
21
+ * await logger.error(err, { route: "/api/checkout" });
22
+ * }
23
+ * ```
24
+ *
25
+ * @param options Logger configuration. `botToken` and `chatId` fall back to the
26
+ * `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID` environment variables.
27
+ * @returns A {@linkcode TelegramLogger}.
28
+ */
29
+ export declare function createTelegramLogger(options?: TelegramLoggerOptions): TelegramLogger;
package/dist/mod.cjs ADDED
@@ -0,0 +1,312 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ function __accessProp(key) {
6
+ return this[key];
7
+ }
8
+ var __toCommonJS = (from) => {
9
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
10
+ if (entry)
11
+ return entry;
12
+ entry = __defProp({}, "__esModule", { value: true });
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (var key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(entry, key))
16
+ __defProp(entry, key, {
17
+ get: __accessProp.bind(from, key),
18
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
19
+ });
20
+ }
21
+ __moduleCache.set(from, entry);
22
+ return entry;
23
+ };
24
+ var __moduleCache;
25
+ var __returnValue = (v) => v;
26
+ function __exportSetter(name, newValue) {
27
+ this[name] = __returnValue.bind(null, newValue);
28
+ }
29
+ var __export = (target, all) => {
30
+ for (var name in all)
31
+ __defProp(target, name, {
32
+ get: all[name],
33
+ enumerable: true,
34
+ configurable: true,
35
+ set: __exportSetter.bind(all, name)
36
+ });
37
+ };
38
+
39
+ // src/mod.ts
40
+ var exports_mod = {};
41
+ __export(exports_mod, {
42
+ MAX_MESSAGE_LENGTH: () => MAX_MESSAGE_LENGTH,
43
+ TelegramLoggerError: () => TelegramLoggerError,
44
+ createTelegramLogger: () => createTelegramLogger,
45
+ escapeHtml: () => escapeHtml,
46
+ formatContact: () => formatContact,
47
+ formatLog: () => formatLog
48
+ });
49
+ module.exports = __toCommonJS(exports_mod);
50
+
51
+ // src/format.ts
52
+ var MAX_MESSAGE_LENGTH = 4096;
53
+ var ICONS = {
54
+ debug: "โšช",
55
+ info: "\uD83D\uDD35",
56
+ success: "\uD83D\uDFE2",
57
+ warn: "\uD83D\uDFE1",
58
+ error: "\uD83D\uDD34",
59
+ fatal: "\uD83D\uDC80",
60
+ contact: "\uD83D\uDCE9"
61
+ };
62
+ var LABELS = {
63
+ debug: "DEBUG",
64
+ info: "INFO",
65
+ success: "SUCCESS",
66
+ warn: "WARNING",
67
+ error: "ERROR",
68
+ fatal: "FATAL",
69
+ contact: "NEW CONTACT"
70
+ };
71
+ var CONTACT_FIELDS = ["name", "email", "phone", "subject"];
72
+ function escapeHtml(text) {
73
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
74
+ }
75
+ function truncate(text, max) {
76
+ if (text.length <= max)
77
+ return text;
78
+ return `${text.slice(0, Math.max(0, max - 1))}โ€ฆ`;
79
+ }
80
+ function stringify(value) {
81
+ if (typeof value === "string")
82
+ return value;
83
+ if (value instanceof Error)
84
+ return `${value.name}: ${value.message}`;
85
+ if (value === null || typeof value !== "object") {
86
+ return String(value);
87
+ }
88
+ try {
89
+ const json = JSON.stringify(value, (_key, v) => typeof v === "bigint" ? v.toString() : v);
90
+ return json ?? String(value);
91
+ } catch {
92
+ return String(value);
93
+ }
94
+ }
95
+ function describeInput(input) {
96
+ if (input instanceof Error) {
97
+ const stackLines = (input.stack ?? "").split(`
98
+ `);
99
+ const frames = stackLines[0]?.includes(input.message) ? stackLines.slice(1) : stackLines;
100
+ let block = frames.map((l) => l.trim()).filter(Boolean).join(`
101
+ `);
102
+ let cause = input.cause;
103
+ for (let depth = 0;cause !== undefined && depth < 3; depth++) {
104
+ block += `${block ? `
105
+ ` : ""}Caused by: ${stringify(cause)}`;
106
+ cause = cause instanceof Error ? cause.cause : undefined;
107
+ }
108
+ return { title: `${input.name}: ${input.message}`, block: block || undefined };
109
+ }
110
+ if (typeof input === "string") {
111
+ const [first = "", ...rest] = input.split(`
112
+ `);
113
+ return { title: first, body: rest.join(`
114
+ `).trim() || undefined };
115
+ }
116
+ return { title: stringify(input) };
117
+ }
118
+ function header(kind, appName, environment) {
119
+ return [`${ICONS[kind]} <b>${LABELS[kind]}</b>`, appName, environment].filter(Boolean).map((part, i) => i === 0 ? part : escapeHtml(part)).join(" ยท ");
120
+ }
121
+ function render(head, parts, budget, timestamp) {
122
+ const lines = [head, `<b>${escapeHtml(truncate(parts.title, budget))}</b>`];
123
+ if (parts.body)
124
+ lines.push("", escapeHtml(truncate(parts.body, budget * 2)));
125
+ if (parts.fields.length > 0) {
126
+ lines.push("");
127
+ for (const [key, value] of parts.fields) {
128
+ lines.push(`<b>${escapeHtml(truncate(key, 64))}:</b> ${escapeHtml(truncate(value, budget))}`);
129
+ }
130
+ }
131
+ if (parts.block) {
132
+ const tag = parts.blockIsQuote ? "blockquote" : "pre";
133
+ lines.push("", `<${tag}>${escapeHtml(truncate(parts.block, budget * 3))}</${tag}>`);
134
+ }
135
+ lines.push("", `\uD83D\uDD52 ${timestamp}`);
136
+ return lines.join(`
137
+ `);
138
+ }
139
+ function fit(head, parts, timestamp) {
140
+ for (let budget = 1200;budget >= 16; budget = Math.floor(budget / 2)) {
141
+ const text = render(head, parts, budget, timestamp);
142
+ if (text.length <= MAX_MESSAGE_LENGTH)
143
+ return text;
144
+ }
145
+ return render(head, { title: parts.title, fields: [] }, 16, timestamp);
146
+ }
147
+ function formatLog(kind, input, meta = {}, ctx = {}) {
148
+ const described = describeInput(input);
149
+ const parts = {
150
+ ...described,
151
+ title: ctx.title ?? described.title,
152
+ body: ctx.title !== undefined && described.title !== ctx.title ? [described.title, described.body].filter(Boolean).join(`
153
+ `) : described.body,
154
+ fields: Object.entries(meta).filter(([, v]) => v !== undefined).map(([k, v]) => [k, stringify(v)])
155
+ };
156
+ const timestamp = (ctx.now ?? new Date).toISOString();
157
+ return fit(header(kind, ctx.appName, ctx.environment), parts, timestamp);
158
+ }
159
+ function formatContact(data, ctx = {}) {
160
+ const { message, ...rest } = data;
161
+ const known = CONTACT_FIELDS.filter((f) => rest[f] !== undefined && rest[f] !== "");
162
+ const extra = Object.keys(rest).filter((k) => !CONTACT_FIELDS.includes(k) && rest[k] !== undefined);
163
+ const parts = {
164
+ title: ctx.title ?? (typeof data.name === "string" && data.name ? `From ${data.name}` : "New message"),
165
+ fields: [...known, ...extra].map((k) => [k, stringify(rest[k])]),
166
+ block: message === undefined || message === "" ? undefined : stringify(message),
167
+ blockIsQuote: true
168
+ };
169
+ const timestamp = (ctx.now ?? new Date).toISOString();
170
+ return fit(header("contact", ctx.appName, ctx.environment), parts, timestamp);
171
+ }
172
+ // src/types.ts
173
+ class TelegramLoggerError extends Error {
174
+ reason;
175
+ status;
176
+ constructor(reason, message, options = {}) {
177
+ super(message, { cause: options.cause });
178
+ this.name = "TelegramLoggerError";
179
+ this.reason = reason;
180
+ this.status = options.status;
181
+ }
182
+ }
183
+
184
+ // src/logger.ts
185
+ var LEVEL_RANK = {
186
+ debug: 10,
187
+ info: 20,
188
+ success: 20,
189
+ warn: 30,
190
+ error: 40,
191
+ fatal: 50
192
+ };
193
+ var DEFAULT_TIMEOUT_MS = 5000;
194
+ var DEFAULT_MAX_RETRY_AFTER_SECONDS = 5;
195
+ var DEFAULT_API_BASE_URL = "https://api.telegram.org";
196
+ function readEnv(name) {
197
+ try {
198
+ const g = globalThis;
199
+ return g.process?.env?.[name] ?? g.Deno?.env?.get(name) ?? undefined;
200
+ } catch {
201
+ return;
202
+ }
203
+ }
204
+ function sleep(ms) {
205
+ return new Promise((resolve) => setTimeout(resolve, ms));
206
+ }
207
+ function createTelegramLogger(options = {}) {
208
+ const {
209
+ appName,
210
+ environment,
211
+ enabled = true,
212
+ minLevel = "debug",
213
+ timeoutMs = DEFAULT_TIMEOUT_MS,
214
+ maxRetryAfterSeconds = DEFAULT_MAX_RETRY_AFTER_SECONDS,
215
+ disableNotification = false,
216
+ messageThreadId,
217
+ onError,
218
+ apiBaseUrl = DEFAULT_API_BASE_URL
219
+ } = options;
220
+ function fail(result) {
221
+ if (result.error && onError) {
222
+ try {
223
+ onError(result.error);
224
+ } catch {}
225
+ }
226
+ return result;
227
+ }
228
+ async function deliver(kind, text, sendOptions) {
229
+ const botToken = options.botToken ?? readEnv("TELEGRAM_BOT_TOKEN");
230
+ const chatId = sendOptions.chatId ?? options.chatIds?.[kind] ?? options.chatId ?? readEnv("TELEGRAM_CHAT_ID");
231
+ if (!botToken || chatId === undefined || chatId === "") {
232
+ return fail({
233
+ ok: false,
234
+ reason: "missing_config",
235
+ error: new TelegramLoggerError("missing_config", "Missing botToken or chatId. Pass them to createTelegramLogger() or set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID.")
236
+ });
237
+ }
238
+ const doFetch = options.fetch ?? globalThis.fetch;
239
+ const threadId = sendOptions.messageThreadId ?? messageThreadId;
240
+ const body = JSON.stringify({
241
+ chat_id: chatId,
242
+ text,
243
+ parse_mode: "HTML",
244
+ link_preview_options: { is_disabled: true },
245
+ disable_notification: sendOptions.disableNotification ?? disableNotification,
246
+ ...threadId === undefined ? {} : { message_thread_id: threadId }
247
+ });
248
+ for (let attempt = 0;; attempt++) {
249
+ let response;
250
+ try {
251
+ response = await doFetch(`${apiBaseUrl}/bot${botToken}/sendMessage`, {
252
+ method: "POST",
253
+ headers: { "content-type": "application/json" },
254
+ body,
255
+ signal: AbortSignal.timeout(timeoutMs)
256
+ });
257
+ } catch (cause) {
258
+ const isTimeout = cause instanceof Error && (cause.name === "TimeoutError" || cause.name === "AbortError");
259
+ const reason = isTimeout ? "timeout" : "network_error";
260
+ const message = isTimeout ? `Telegram did not answer within ${timeoutMs}ms` : "Could not reach Telegram";
261
+ return fail({ ok: false, reason, error: new TelegramLoggerError(reason, message, { cause }) });
262
+ }
263
+ let data;
264
+ try {
265
+ data = await response.json();
266
+ } catch {
267
+ data = undefined;
268
+ }
269
+ if (response.ok && data?.ok && data.result) {
270
+ return { ok: true, messageId: data.result.message_id };
271
+ }
272
+ const retryAfter = data?.parameters?.retry_after;
273
+ if (response.status === 429 && attempt === 0 && retryAfter !== undefined && retryAfter <= maxRetryAfterSeconds) {
274
+ await sleep(retryAfter * 1000);
275
+ continue;
276
+ }
277
+ return fail({
278
+ ok: false,
279
+ reason: "telegram_error",
280
+ error: new TelegramLoggerError("telegram_error", `Telegram API error ${response.status}: ${data?.description ?? response.statusText}`, { status: response.status })
281
+ });
282
+ }
283
+ }
284
+ function log(level, input, meta, sendOptions = {}) {
285
+ if (!enabled)
286
+ return Promise.resolve({ ok: false, reason: "disabled" });
287
+ if (LEVEL_RANK[level] < LEVEL_RANK[minLevel])
288
+ return Promise.resolve({ ok: false, reason: "below_min_level" });
289
+ const text = formatLog(level, input, meta, { appName, environment, title: sendOptions.title });
290
+ return deliver(level, text, sendOptions);
291
+ }
292
+ return {
293
+ log,
294
+ debug: (input, meta, opts) => log("debug", input, meta, opts),
295
+ info: (input, meta, opts) => log("info", input, meta, opts),
296
+ success: (input, meta, opts) => log("success", input, meta, opts),
297
+ warn: (input, meta, opts) => log("warn", input, meta, opts),
298
+ error: (input, meta, opts) => log("error", input, meta, opts),
299
+ fatal: (input, meta, opts) => log("fatal", input, meta, opts),
300
+ contact(data, sendOptions = {}) {
301
+ if (!enabled)
302
+ return Promise.resolve({ ok: false, reason: "disabled" });
303
+ const text = formatContact(data, { appName, environment, title: sendOptions.title });
304
+ return deliver("contact", text, sendOptions);
305
+ },
306
+ send(html, sendOptions = {}) {
307
+ if (!enabled)
308
+ return Promise.resolve({ ok: false, reason: "disabled" });
309
+ return deliver("info", html, sendOptions);
310
+ }
311
+ };
312
+ }
package/dist/mod.d.cts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Zero-dependency logger that sends errors, warnings, events and contact-form
3
+ * submissions to your Telegram chats and channels.
4
+ *
5
+ * Works on Node.js 18+, Bun, Deno, Cloudflare Workers and Vercel/Next.js Edge.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { createTelegramLogger } from "@galhardoalex/telegram-logger";
10
+ *
11
+ * const logger = createTelegramLogger({
12
+ * botToken: process.env.TELEGRAM_BOT_TOKEN,
13
+ * chatId: process.env.TELEGRAM_CHAT_ID,
14
+ * appName: "my-site",
15
+ * environment: "production",
16
+ * });
17
+ *
18
+ * await logger.error(new Error("Payment failed"), { orderId: 42 });
19
+ * await logger.contact({ name: "Ana", email: "ana@example.com", message: "Hi!" });
20
+ * ```
21
+ *
22
+ * @module
23
+ */
24
+ export { escapeHtml, type FormatContext, formatContact, formatLog, MAX_MESSAGE_LENGTH } from "./format.cjs";
25
+ export { createTelegramLogger } from "./logger.cjs";
26
+ export { type ChatId, type ContactData, type FailureReason, type LogLevel, type LogMeta, type MessageKind, type SendOptions, type SendResult, type TelegramLogger, TelegramLoggerError, type TelegramLoggerOptions, } from "./types.cjs";
package/dist/mod.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Zero-dependency logger that sends errors, warnings, events and contact-form
3
+ * submissions to your Telegram chats and channels.
4
+ *
5
+ * Works on Node.js 18+, Bun, Deno, Cloudflare Workers and Vercel/Next.js Edge.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { createTelegramLogger } from "@galhardoalex/telegram-logger";
10
+ *
11
+ * const logger = createTelegramLogger({
12
+ * botToken: process.env.TELEGRAM_BOT_TOKEN,
13
+ * chatId: process.env.TELEGRAM_CHAT_ID,
14
+ * appName: "my-site",
15
+ * environment: "production",
16
+ * });
17
+ *
18
+ * await logger.error(new Error("Payment failed"), { orderId: 42 });
19
+ * await logger.contact({ name: "Ana", email: "ana@example.com", message: "Hi!" });
20
+ * ```
21
+ *
22
+ * @module
23
+ */
24
+ export { escapeHtml, type FormatContext, formatContact, formatLog, MAX_MESSAGE_LENGTH } from "./format.js";
25
+ export { createTelegramLogger } from "./logger.js";
26
+ export { type ChatId, type ContactData, type FailureReason, type LogLevel, type LogMeta, type MessageKind, type SendOptions, type SendResult, type TelegramLogger, TelegramLoggerError, type TelegramLoggerOptions, } from "./types.js";
package/dist/mod.js ADDED
@@ -0,0 +1,270 @@
1
+ // src/format.ts
2
+ var MAX_MESSAGE_LENGTH = 4096;
3
+ var ICONS = {
4
+ debug: "โšช",
5
+ info: "\uD83D\uDD35",
6
+ success: "\uD83D\uDFE2",
7
+ warn: "\uD83D\uDFE1",
8
+ error: "\uD83D\uDD34",
9
+ fatal: "\uD83D\uDC80",
10
+ contact: "\uD83D\uDCE9"
11
+ };
12
+ var LABELS = {
13
+ debug: "DEBUG",
14
+ info: "INFO",
15
+ success: "SUCCESS",
16
+ warn: "WARNING",
17
+ error: "ERROR",
18
+ fatal: "FATAL",
19
+ contact: "NEW CONTACT"
20
+ };
21
+ var CONTACT_FIELDS = ["name", "email", "phone", "subject"];
22
+ function escapeHtml(text) {
23
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
24
+ }
25
+ function truncate(text, max) {
26
+ if (text.length <= max)
27
+ return text;
28
+ return `${text.slice(0, Math.max(0, max - 1))}โ€ฆ`;
29
+ }
30
+ function stringify(value) {
31
+ if (typeof value === "string")
32
+ return value;
33
+ if (value instanceof Error)
34
+ return `${value.name}: ${value.message}`;
35
+ if (value === null || typeof value !== "object") {
36
+ return String(value);
37
+ }
38
+ try {
39
+ const json = JSON.stringify(value, (_key, v) => typeof v === "bigint" ? v.toString() : v);
40
+ return json ?? String(value);
41
+ } catch {
42
+ return String(value);
43
+ }
44
+ }
45
+ function describeInput(input) {
46
+ if (input instanceof Error) {
47
+ const stackLines = (input.stack ?? "").split(`
48
+ `);
49
+ const frames = stackLines[0]?.includes(input.message) ? stackLines.slice(1) : stackLines;
50
+ let block = frames.map((l) => l.trim()).filter(Boolean).join(`
51
+ `);
52
+ let cause = input.cause;
53
+ for (let depth = 0;cause !== undefined && depth < 3; depth++) {
54
+ block += `${block ? `
55
+ ` : ""}Caused by: ${stringify(cause)}`;
56
+ cause = cause instanceof Error ? cause.cause : undefined;
57
+ }
58
+ return { title: `${input.name}: ${input.message}`, block: block || undefined };
59
+ }
60
+ if (typeof input === "string") {
61
+ const [first = "", ...rest] = input.split(`
62
+ `);
63
+ return { title: first, body: rest.join(`
64
+ `).trim() || undefined };
65
+ }
66
+ return { title: stringify(input) };
67
+ }
68
+ function header(kind, appName, environment) {
69
+ return [`${ICONS[kind]} <b>${LABELS[kind]}</b>`, appName, environment].filter(Boolean).map((part, i) => i === 0 ? part : escapeHtml(part)).join(" ยท ");
70
+ }
71
+ function render(head, parts, budget, timestamp) {
72
+ const lines = [head, `<b>${escapeHtml(truncate(parts.title, budget))}</b>`];
73
+ if (parts.body)
74
+ lines.push("", escapeHtml(truncate(parts.body, budget * 2)));
75
+ if (parts.fields.length > 0) {
76
+ lines.push("");
77
+ for (const [key, value] of parts.fields) {
78
+ lines.push(`<b>${escapeHtml(truncate(key, 64))}:</b> ${escapeHtml(truncate(value, budget))}`);
79
+ }
80
+ }
81
+ if (parts.block) {
82
+ const tag = parts.blockIsQuote ? "blockquote" : "pre";
83
+ lines.push("", `<${tag}>${escapeHtml(truncate(parts.block, budget * 3))}</${tag}>`);
84
+ }
85
+ lines.push("", `\uD83D\uDD52 ${timestamp}`);
86
+ return lines.join(`
87
+ `);
88
+ }
89
+ function fit(head, parts, timestamp) {
90
+ for (let budget = 1200;budget >= 16; budget = Math.floor(budget / 2)) {
91
+ const text = render(head, parts, budget, timestamp);
92
+ if (text.length <= MAX_MESSAGE_LENGTH)
93
+ return text;
94
+ }
95
+ return render(head, { title: parts.title, fields: [] }, 16, timestamp);
96
+ }
97
+ function formatLog(kind, input, meta = {}, ctx = {}) {
98
+ const described = describeInput(input);
99
+ const parts = {
100
+ ...described,
101
+ title: ctx.title ?? described.title,
102
+ body: ctx.title !== undefined && described.title !== ctx.title ? [described.title, described.body].filter(Boolean).join(`
103
+ `) : described.body,
104
+ fields: Object.entries(meta).filter(([, v]) => v !== undefined).map(([k, v]) => [k, stringify(v)])
105
+ };
106
+ const timestamp = (ctx.now ?? new Date).toISOString();
107
+ return fit(header(kind, ctx.appName, ctx.environment), parts, timestamp);
108
+ }
109
+ function formatContact(data, ctx = {}) {
110
+ const { message, ...rest } = data;
111
+ const known = CONTACT_FIELDS.filter((f) => rest[f] !== undefined && rest[f] !== "");
112
+ const extra = Object.keys(rest).filter((k) => !CONTACT_FIELDS.includes(k) && rest[k] !== undefined);
113
+ const parts = {
114
+ title: ctx.title ?? (typeof data.name === "string" && data.name ? `From ${data.name}` : "New message"),
115
+ fields: [...known, ...extra].map((k) => [k, stringify(rest[k])]),
116
+ block: message === undefined || message === "" ? undefined : stringify(message),
117
+ blockIsQuote: true
118
+ };
119
+ const timestamp = (ctx.now ?? new Date).toISOString();
120
+ return fit(header("contact", ctx.appName, ctx.environment), parts, timestamp);
121
+ }
122
+ // src/types.ts
123
+ class TelegramLoggerError extends Error {
124
+ reason;
125
+ status;
126
+ constructor(reason, message, options = {}) {
127
+ super(message, { cause: options.cause });
128
+ this.name = "TelegramLoggerError";
129
+ this.reason = reason;
130
+ this.status = options.status;
131
+ }
132
+ }
133
+
134
+ // src/logger.ts
135
+ var LEVEL_RANK = {
136
+ debug: 10,
137
+ info: 20,
138
+ success: 20,
139
+ warn: 30,
140
+ error: 40,
141
+ fatal: 50
142
+ };
143
+ var DEFAULT_TIMEOUT_MS = 5000;
144
+ var DEFAULT_MAX_RETRY_AFTER_SECONDS = 5;
145
+ var DEFAULT_API_BASE_URL = "https://api.telegram.org";
146
+ function readEnv(name) {
147
+ try {
148
+ const g = globalThis;
149
+ return g.process?.env?.[name] ?? g.Deno?.env?.get(name) ?? undefined;
150
+ } catch {
151
+ return;
152
+ }
153
+ }
154
+ function sleep(ms) {
155
+ return new Promise((resolve) => setTimeout(resolve, ms));
156
+ }
157
+ function createTelegramLogger(options = {}) {
158
+ const {
159
+ appName,
160
+ environment,
161
+ enabled = true,
162
+ minLevel = "debug",
163
+ timeoutMs = DEFAULT_TIMEOUT_MS,
164
+ maxRetryAfterSeconds = DEFAULT_MAX_RETRY_AFTER_SECONDS,
165
+ disableNotification = false,
166
+ messageThreadId,
167
+ onError,
168
+ apiBaseUrl = DEFAULT_API_BASE_URL
169
+ } = options;
170
+ function fail(result) {
171
+ if (result.error && onError) {
172
+ try {
173
+ onError(result.error);
174
+ } catch {}
175
+ }
176
+ return result;
177
+ }
178
+ async function deliver(kind, text, sendOptions) {
179
+ const botToken = options.botToken ?? readEnv("TELEGRAM_BOT_TOKEN");
180
+ const chatId = sendOptions.chatId ?? options.chatIds?.[kind] ?? options.chatId ?? readEnv("TELEGRAM_CHAT_ID");
181
+ if (!botToken || chatId === undefined || chatId === "") {
182
+ return fail({
183
+ ok: false,
184
+ reason: "missing_config",
185
+ error: new TelegramLoggerError("missing_config", "Missing botToken or chatId. Pass them to createTelegramLogger() or set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID.")
186
+ });
187
+ }
188
+ const doFetch = options.fetch ?? globalThis.fetch;
189
+ const threadId = sendOptions.messageThreadId ?? messageThreadId;
190
+ const body = JSON.stringify({
191
+ chat_id: chatId,
192
+ text,
193
+ parse_mode: "HTML",
194
+ link_preview_options: { is_disabled: true },
195
+ disable_notification: sendOptions.disableNotification ?? disableNotification,
196
+ ...threadId === undefined ? {} : { message_thread_id: threadId }
197
+ });
198
+ for (let attempt = 0;; attempt++) {
199
+ let response;
200
+ try {
201
+ response = await doFetch(`${apiBaseUrl}/bot${botToken}/sendMessage`, {
202
+ method: "POST",
203
+ headers: { "content-type": "application/json" },
204
+ body,
205
+ signal: AbortSignal.timeout(timeoutMs)
206
+ });
207
+ } catch (cause) {
208
+ const isTimeout = cause instanceof Error && (cause.name === "TimeoutError" || cause.name === "AbortError");
209
+ const reason = isTimeout ? "timeout" : "network_error";
210
+ const message = isTimeout ? `Telegram did not answer within ${timeoutMs}ms` : "Could not reach Telegram";
211
+ return fail({ ok: false, reason, error: new TelegramLoggerError(reason, message, { cause }) });
212
+ }
213
+ let data;
214
+ try {
215
+ data = await response.json();
216
+ } catch {
217
+ data = undefined;
218
+ }
219
+ if (response.ok && data?.ok && data.result) {
220
+ return { ok: true, messageId: data.result.message_id };
221
+ }
222
+ const retryAfter = data?.parameters?.retry_after;
223
+ if (response.status === 429 && attempt === 0 && retryAfter !== undefined && retryAfter <= maxRetryAfterSeconds) {
224
+ await sleep(retryAfter * 1000);
225
+ continue;
226
+ }
227
+ return fail({
228
+ ok: false,
229
+ reason: "telegram_error",
230
+ error: new TelegramLoggerError("telegram_error", `Telegram API error ${response.status}: ${data?.description ?? response.statusText}`, { status: response.status })
231
+ });
232
+ }
233
+ }
234
+ function log(level, input, meta, sendOptions = {}) {
235
+ if (!enabled)
236
+ return Promise.resolve({ ok: false, reason: "disabled" });
237
+ if (LEVEL_RANK[level] < LEVEL_RANK[minLevel])
238
+ return Promise.resolve({ ok: false, reason: "below_min_level" });
239
+ const text = formatLog(level, input, meta, { appName, environment, title: sendOptions.title });
240
+ return deliver(level, text, sendOptions);
241
+ }
242
+ return {
243
+ log,
244
+ debug: (input, meta, opts) => log("debug", input, meta, opts),
245
+ info: (input, meta, opts) => log("info", input, meta, opts),
246
+ success: (input, meta, opts) => log("success", input, meta, opts),
247
+ warn: (input, meta, opts) => log("warn", input, meta, opts),
248
+ error: (input, meta, opts) => log("error", input, meta, opts),
249
+ fatal: (input, meta, opts) => log("fatal", input, meta, opts),
250
+ contact(data, sendOptions = {}) {
251
+ if (!enabled)
252
+ return Promise.resolve({ ok: false, reason: "disabled" });
253
+ const text = formatContact(data, { appName, environment, title: sendOptions.title });
254
+ return deliver("contact", text, sendOptions);
255
+ },
256
+ send(html, sendOptions = {}) {
257
+ if (!enabled)
258
+ return Promise.resolve({ ok: false, reason: "disabled" });
259
+ return deliver("info", html, sendOptions);
260
+ }
261
+ };
262
+ }
263
+ export {
264
+ MAX_MESSAGE_LENGTH,
265
+ TelegramLoggerError,
266
+ createTelegramLogger,
267
+ escapeHtml,
268
+ formatContact,
269
+ formatLog
270
+ };
@@ -0,0 +1,134 @@
1
+ /** Severity levels, from least to most severe. */
2
+ export type LogLevel = "debug" | "info" | "success" | "warn" | "error" | "fatal";
3
+ /** Every kind of message the logger can send: the log levels plus `contact`. */
4
+ export type MessageKind = LogLevel | "contact";
5
+ /** A Telegram chat identifier: a numeric id (e.g. `-1001234567890`) or a public `@channelusername`. */
6
+ export type ChatId = string | number;
7
+ /** Arbitrary key/value context attached to a message (route, userId, request id, ...). */
8
+ export type LogMeta = Record<string, unknown>;
9
+ /** Configuration for {@linkcode createTelegramLogger}. */
10
+ export interface TelegramLoggerOptions {
11
+ /**
12
+ * Bot token from [@BotFather](https://t.me/BotFather).
13
+ * Falls back to the `TELEGRAM_BOT_TOKEN` environment variable.
14
+ */
15
+ botToken?: string;
16
+ /**
17
+ * Default chat that receives every message.
18
+ * Falls back to the `TELEGRAM_CHAT_ID` environment variable.
19
+ */
20
+ chatId?: ChatId;
21
+ /**
22
+ * Per-kind chat overrides. Kinds without an entry use {@linkcode TelegramLoggerOptions.chatId}.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * chatIds: { error: "-100111", fatal: "-100111", contact: "-100222" }
27
+ * ```
28
+ */
29
+ chatIds?: Partial<Record<MessageKind, ChatId>>;
30
+ /** App name shown in the header of every message (e.g. `"my-site"`). */
31
+ appName?: string;
32
+ /** Environment shown in the header of every message (e.g. `"production"`). */
33
+ environment?: string;
34
+ /** Set to `false` to turn the logger into a no-op (e.g. in local development). Defaults to `true`. */
35
+ enabled?: boolean;
36
+ /** Log levels below this one are skipped. `contact` messages are never filtered. Defaults to `"debug"`. */
37
+ minLevel?: LogLevel;
38
+ /** Timeout in milliseconds for each request to the Telegram API. Defaults to `5000`. */
39
+ timeoutMs?: number;
40
+ /**
41
+ * When Telegram answers `429 Too Many Requests`, wait for `retry_after` and try once more,
42
+ * as long as the wait is at most this many seconds. Set to `0` to disable. Defaults to `5`.
43
+ */
44
+ maxRetryAfterSeconds?: number;
45
+ /** Send messages silently (no notification sound). Defaults to `false`. */
46
+ disableNotification?: boolean;
47
+ /** Topic (thread) id when the target chat is a forum supergroup. */
48
+ messageThreadId?: number;
49
+ /** Called whenever a message fails to send. The logger itself never throws. */
50
+ onError?: (error: TelegramLoggerError) => void;
51
+ /** Custom `fetch` implementation. Defaults to `globalThis.fetch`. */
52
+ fetch?: (url: string, init: RequestInit) => Promise<Response>;
53
+ /** Telegram Bot API base URL. Defaults to `"https://api.telegram.org"`. */
54
+ apiBaseUrl?: string;
55
+ }
56
+ /** Per-call overrides accepted by every logger method. */
57
+ export interface SendOptions {
58
+ /** Send this message to a different chat. */
59
+ chatId?: ChatId;
60
+ /** Replace the default title (first bold line) of the message. */
61
+ title?: string;
62
+ /** Send this message silently. */
63
+ disableNotification?: boolean;
64
+ /** Topic (thread) id when the target chat is a forum supergroup. */
65
+ messageThreadId?: number;
66
+ }
67
+ /** Data captured by a typical contact form. Extra fields are listed below the known ones. */
68
+ export interface ContactData {
69
+ /** Name of the person who submitted the form. */
70
+ name?: string;
71
+ /** E-mail address. */
72
+ email?: string;
73
+ /** Phone number. */
74
+ phone?: string;
75
+ /** Subject line. */
76
+ subject?: string;
77
+ /** Free-text message. */
78
+ message?: string;
79
+ /** Any other form field. */
80
+ [field: string]: unknown;
81
+ }
82
+ /** Why a message was not delivered. */
83
+ export type FailureReason = "disabled" | "below_min_level" | "missing_config" | "telegram_error" | "network_error" | "timeout";
84
+ /** Outcome of a send. Logger methods always resolve, never reject. */
85
+ export type SendResult = {
86
+ /** The message was delivered. */
87
+ ok: true;
88
+ /** Id of the message created in the chat. */
89
+ messageId: number;
90
+ } | {
91
+ /** The message was not delivered. */
92
+ ok: false;
93
+ /** Why it was not delivered. */
94
+ reason: FailureReason;
95
+ /** Details of the failure, when there is an error behind it. */
96
+ error?: TelegramLoggerError;
97
+ };
98
+ /** Error describing a failed delivery. Passed to `onError` and returned in {@linkcode SendResult}. */
99
+ export declare class TelegramLoggerError extends Error {
100
+ /** Machine-readable failure category. */
101
+ readonly reason: FailureReason;
102
+ /** HTTP status code returned by Telegram, when there was a response. */
103
+ readonly status?: number;
104
+ /**
105
+ * @param reason Machine-readable failure category.
106
+ * @param message Human-readable description.
107
+ * @param options Optional HTTP status and underlying cause.
108
+ */
109
+ constructor(reason: FailureReason, message: string, options?: {
110
+ status?: number;
111
+ cause?: unknown;
112
+ });
113
+ }
114
+ /** The object returned by {@linkcode createTelegramLogger}. */
115
+ export interface TelegramLogger {
116
+ /** Send a message with an explicit level. */
117
+ log(level: LogLevel, input: unknown, meta?: LogMeta, options?: SendOptions): Promise<SendResult>;
118
+ /** โšช Debug message. */
119
+ debug(input: unknown, meta?: LogMeta, options?: SendOptions): Promise<SendResult>;
120
+ /** ๐Ÿ”ต Informational message. */
121
+ info(input: unknown, meta?: LogMeta, options?: SendOptions): Promise<SendResult>;
122
+ /** ๐ŸŸข Something good happened (a sale, a deploy, a sign-up). */
123
+ success(input: unknown, meta?: LogMeta, options?: SendOptions): Promise<SendResult>;
124
+ /** ๐ŸŸก Warning. */
125
+ warn(input: unknown, meta?: LogMeta, options?: SendOptions): Promise<SendResult>;
126
+ /** ๐Ÿ”ด Error. Pass the caught value directly: `Error` objects include their stack and cause. */
127
+ error(input: unknown, meta?: LogMeta, options?: SendOptions): Promise<SendResult>;
128
+ /** ๐Ÿ’€ Fatal error. */
129
+ fatal(input: unknown, meta?: LogMeta, options?: SendOptions): Promise<SendResult>;
130
+ /** ๐Ÿ“ฉ Contact form submission. Never filtered by `minLevel`. */
131
+ contact(data: ContactData, options?: SendOptions): Promise<SendResult>;
132
+ /** Send raw Telegram-HTML text as is, without header or formatting. You must escape user input yourself (see `escapeHtml`). */
133
+ send(html: string, options?: SendOptions): Promise<SendResult>;
134
+ }
@@ -0,0 +1,134 @@
1
+ /** Severity levels, from least to most severe. */
2
+ export type LogLevel = "debug" | "info" | "success" | "warn" | "error" | "fatal";
3
+ /** Every kind of message the logger can send: the log levels plus `contact`. */
4
+ export type MessageKind = LogLevel | "contact";
5
+ /** A Telegram chat identifier: a numeric id (e.g. `-1001234567890`) or a public `@channelusername`. */
6
+ export type ChatId = string | number;
7
+ /** Arbitrary key/value context attached to a message (route, userId, request id, ...). */
8
+ export type LogMeta = Record<string, unknown>;
9
+ /** Configuration for {@linkcode createTelegramLogger}. */
10
+ export interface TelegramLoggerOptions {
11
+ /**
12
+ * Bot token from [@BotFather](https://t.me/BotFather).
13
+ * Falls back to the `TELEGRAM_BOT_TOKEN` environment variable.
14
+ */
15
+ botToken?: string;
16
+ /**
17
+ * Default chat that receives every message.
18
+ * Falls back to the `TELEGRAM_CHAT_ID` environment variable.
19
+ */
20
+ chatId?: ChatId;
21
+ /**
22
+ * Per-kind chat overrides. Kinds without an entry use {@linkcode TelegramLoggerOptions.chatId}.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * chatIds: { error: "-100111", fatal: "-100111", contact: "-100222" }
27
+ * ```
28
+ */
29
+ chatIds?: Partial<Record<MessageKind, ChatId>>;
30
+ /** App name shown in the header of every message (e.g. `"my-site"`). */
31
+ appName?: string;
32
+ /** Environment shown in the header of every message (e.g. `"production"`). */
33
+ environment?: string;
34
+ /** Set to `false` to turn the logger into a no-op (e.g. in local development). Defaults to `true`. */
35
+ enabled?: boolean;
36
+ /** Log levels below this one are skipped. `contact` messages are never filtered. Defaults to `"debug"`. */
37
+ minLevel?: LogLevel;
38
+ /** Timeout in milliseconds for each request to the Telegram API. Defaults to `5000`. */
39
+ timeoutMs?: number;
40
+ /**
41
+ * When Telegram answers `429 Too Many Requests`, wait for `retry_after` and try once more,
42
+ * as long as the wait is at most this many seconds. Set to `0` to disable. Defaults to `5`.
43
+ */
44
+ maxRetryAfterSeconds?: number;
45
+ /** Send messages silently (no notification sound). Defaults to `false`. */
46
+ disableNotification?: boolean;
47
+ /** Topic (thread) id when the target chat is a forum supergroup. */
48
+ messageThreadId?: number;
49
+ /** Called whenever a message fails to send. The logger itself never throws. */
50
+ onError?: (error: TelegramLoggerError) => void;
51
+ /** Custom `fetch` implementation. Defaults to `globalThis.fetch`. */
52
+ fetch?: (url: string, init: RequestInit) => Promise<Response>;
53
+ /** Telegram Bot API base URL. Defaults to `"https://api.telegram.org"`. */
54
+ apiBaseUrl?: string;
55
+ }
56
+ /** Per-call overrides accepted by every logger method. */
57
+ export interface SendOptions {
58
+ /** Send this message to a different chat. */
59
+ chatId?: ChatId;
60
+ /** Replace the default title (first bold line) of the message. */
61
+ title?: string;
62
+ /** Send this message silently. */
63
+ disableNotification?: boolean;
64
+ /** Topic (thread) id when the target chat is a forum supergroup. */
65
+ messageThreadId?: number;
66
+ }
67
+ /** Data captured by a typical contact form. Extra fields are listed below the known ones. */
68
+ export interface ContactData {
69
+ /** Name of the person who submitted the form. */
70
+ name?: string;
71
+ /** E-mail address. */
72
+ email?: string;
73
+ /** Phone number. */
74
+ phone?: string;
75
+ /** Subject line. */
76
+ subject?: string;
77
+ /** Free-text message. */
78
+ message?: string;
79
+ /** Any other form field. */
80
+ [field: string]: unknown;
81
+ }
82
+ /** Why a message was not delivered. */
83
+ export type FailureReason = "disabled" | "below_min_level" | "missing_config" | "telegram_error" | "network_error" | "timeout";
84
+ /** Outcome of a send. Logger methods always resolve, never reject. */
85
+ export type SendResult = {
86
+ /** The message was delivered. */
87
+ ok: true;
88
+ /** Id of the message created in the chat. */
89
+ messageId: number;
90
+ } | {
91
+ /** The message was not delivered. */
92
+ ok: false;
93
+ /** Why it was not delivered. */
94
+ reason: FailureReason;
95
+ /** Details of the failure, when there is an error behind it. */
96
+ error?: TelegramLoggerError;
97
+ };
98
+ /** Error describing a failed delivery. Passed to `onError` and returned in {@linkcode SendResult}. */
99
+ export declare class TelegramLoggerError extends Error {
100
+ /** Machine-readable failure category. */
101
+ readonly reason: FailureReason;
102
+ /** HTTP status code returned by Telegram, when there was a response. */
103
+ readonly status?: number;
104
+ /**
105
+ * @param reason Machine-readable failure category.
106
+ * @param message Human-readable description.
107
+ * @param options Optional HTTP status and underlying cause.
108
+ */
109
+ constructor(reason: FailureReason, message: string, options?: {
110
+ status?: number;
111
+ cause?: unknown;
112
+ });
113
+ }
114
+ /** The object returned by {@linkcode createTelegramLogger}. */
115
+ export interface TelegramLogger {
116
+ /** Send a message with an explicit level. */
117
+ log(level: LogLevel, input: unknown, meta?: LogMeta, options?: SendOptions): Promise<SendResult>;
118
+ /** โšช Debug message. */
119
+ debug(input: unknown, meta?: LogMeta, options?: SendOptions): Promise<SendResult>;
120
+ /** ๐Ÿ”ต Informational message. */
121
+ info(input: unknown, meta?: LogMeta, options?: SendOptions): Promise<SendResult>;
122
+ /** ๐ŸŸข Something good happened (a sale, a deploy, a sign-up). */
123
+ success(input: unknown, meta?: LogMeta, options?: SendOptions): Promise<SendResult>;
124
+ /** ๐ŸŸก Warning. */
125
+ warn(input: unknown, meta?: LogMeta, options?: SendOptions): Promise<SendResult>;
126
+ /** ๐Ÿ”ด Error. Pass the caught value directly: `Error` objects include their stack and cause. */
127
+ error(input: unknown, meta?: LogMeta, options?: SendOptions): Promise<SendResult>;
128
+ /** ๐Ÿ’€ Fatal error. */
129
+ fatal(input: unknown, meta?: LogMeta, options?: SendOptions): Promise<SendResult>;
130
+ /** ๐Ÿ“ฉ Contact form submission. Never filtered by `minLevel`. */
131
+ contact(data: ContactData, options?: SendOptions): Promise<SendResult>;
132
+ /** Send raw Telegram-HTML text as is, without header or formatting. You must escape user input yourself (see `escapeHtml`). */
133
+ send(html: string, options?: SendOptions): Promise<SendResult>;
134
+ }
package/package.json CHANGED
@@ -1,6 +1,75 @@
1
1
  {
2
2
  "name": "@galhardoalex/telegram-logger",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Zero-dependency logger that sends errors, warnings, events and contact-form submissions to your Telegram chats. Works on Node, Bun, Deno, Next.js and edge runtimes.",
5
+ "keywords": [
6
+ "telegram",
7
+ "logger",
8
+ "telegram-bot",
9
+ "notifications",
10
+ "alerts",
11
+ "error-reporting",
12
+ "contact-form",
13
+ "nextjs",
14
+ "edge",
15
+ "typescript"
16
+ ],
17
+ "author": "Alex Galhardo <aleexgvieira@gmail.com>",
18
+ "license": "MIT",
19
+ "homepage": "https://github.com/AlexGalhardo/telegram-logger#readme",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/AlexGalhardo/telegram-logger.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/AlexGalhardo/telegram-logger/issues"
26
+ },
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "main": "./dist/mod.cjs",
30
+ "module": "./dist/mod.js",
31
+ "types": "./dist/mod.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "import": {
35
+ "types": "./dist/mod.d.ts",
36
+ "default": "./dist/mod.js"
37
+ },
38
+ "require": {
39
+ "types": "./dist/mod.d.cts",
40
+ "default": "./dist/mod.cjs"
41
+ }
42
+ },
43
+ "./package.json": "./package.json"
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "README.md",
48
+ "LICENSE"
49
+ ],
50
+ "engines": {
51
+ "node": ">=18"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public",
55
+ "provenance": true
56
+ },
57
+ "scripts": {
58
+ "build": "bun run scripts/build.ts",
59
+ "test": "bun test",
60
+ "typecheck": "tsc -p tsconfig.json",
61
+ "lint": "biome check .",
62
+ "format": "biome check --write .",
63
+ "check:package": "publint --strict && attw --pack . --profile node16",
64
+ "check:jsr": "bunx jsr publish --dry-run --allow-dirty",
65
+ "check": "bun run lint && bun run typecheck && bun test && bun run build && bun run check:package",
66
+ "prepublishOnly": "bun run build"
67
+ },
68
+ "devDependencies": {
69
+ "@arethetypeswrong/cli": "^0.18.5",
70
+ "@biomejs/biome": "^2.5.15",
71
+ "@types/bun": "latest",
72
+ "publint": "^0.3.25",
73
+ "typescript": "^7.0.2"
74
+ }
75
+ }