omni-notify-mcp 1.3.21 → 1.3.23

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.
@@ -0,0 +1,6 @@
1
+ export async function telegramSendErrorHint(r) {
2
+ if (r.status === 401) {
3
+ return "the bot token is invalid or has been revoked — open @BotFather, check the bot behind this token (or create a new one), and update it in the settings UI";
4
+ }
5
+ return r.text();
6
+ }
package/dist/ui/server.js CHANGED
@@ -13,6 +13,7 @@ import nodemailer from "nodemailer";
13
13
  import { PinpointSMSVoiceV2Client, SendTextMessageCommand, DescribePhoneNumbersCommand, DescribeVerifiedDestinationNumbersCommand } from "@aws-sdk/client-pinpoint-sms-voice-v2";
14
14
  import { tmpdir } from "os";
15
15
  import { sendWithRouting, idleReading, formatIdleReading, describePriorityRouting } from "./messaging/notificationEngine.js";
16
+ import { telegramSendErrorHint } from "./messaging/telegramError.js";
16
17
  import { z } from "zod";
17
18
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
19
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -152,32 +153,37 @@ const LEGACY_SECRETS_PATH = join(homedir(), ".notify-mcp-secrets");
152
153
  function e164(s) {
153
154
  return String(s ?? "").replace(/[^\d+]/g, "");
154
155
  }
156
+ // The ONE list of credential-shaped fields, read by both maskSecrets() (out)
157
+ // and mergePreservingSecrets() (back in). Before story #442 these were two
158
+ // hand-written lists of `if`/`guard` calls kept in sync by hand — that's
159
+ // exactly how ntfy.token got added to a channel (story #427's "brainless
160
+ // config" import) without ever being added to either list, and leaked in the
161
+ // clear over `GET /api/config` for as long as the channel existed. A field
162
+ // belongs here the moment its channel gets one that authenticates outbound
163
+ // (a token, password, or bearer webhook URL) — see
164
+ // tests/smoke.test.mjs's naming-convention test, which independently catches
165
+ // a field like this one being added to defaultConfig() but not to this list.
166
+ const SENSITIVE_FIELDS = [
167
+ ["email", "pass"],
168
+ ["email", "clientSecret"],
169
+ ["email", "refreshToken"],
170
+ ["email", "accessToken"],
171
+ ["sms", "secretAccessKey"],
172
+ ["telegram", "token"],
173
+ ["ntfy", "token"],
174
+ ["discord", "webhookUrl"],
175
+ ["discord", "clientSecret"],
176
+ ["slack", "webhookUrl"],
177
+ ["slack", "botToken"],
178
+ ["slack", "clientSecret"],
179
+ ["teams", "webhookUrl"],
180
+ ];
155
181
  function maskSecrets(config) {
156
182
  const c = JSON.parse(JSON.stringify(config));
157
- if (c.email?.pass)
158
- c.email.pass = MASKED;
159
- if (c.email?.clientSecret)
160
- c.email.clientSecret = MASKED;
161
- if (c.email?.refreshToken)
162
- c.email.refreshToken = MASKED;
163
- if (c.email?.accessToken)
164
- c.email.accessToken = MASKED;
165
- if (c.sms?.secretAccessKey)
166
- c.sms.secretAccessKey = MASKED;
167
- if (c.telegram?.token)
168
- c.telegram.token = MASKED;
169
- if (c.discord?.webhookUrl)
170
- c.discord.webhookUrl = MASKED;
171
- if (c.discord?.clientSecret)
172
- c.discord.clientSecret = MASKED;
173
- if (c.slack?.webhookUrl)
174
- c.slack.webhookUrl = MASKED;
175
- if (c.slack?.botToken)
176
- c.slack.botToken = MASKED;
177
- if (c.slack?.clientSecret)
178
- c.slack.clientSecret = MASKED;
179
- if (c.teams?.webhookUrl)
180
- c.teams.webhookUrl = MASKED;
183
+ for (const [sec, field] of SENSITIVE_FIELDS) {
184
+ if (c[sec]?.[field])
185
+ c[sec][field] = MASKED;
186
+ }
181
187
  delete c.whatsapp;
182
188
  return c;
183
189
  }
@@ -192,24 +198,11 @@ function mergePreservingSecrets(existing, update) {
192
198
  }
193
199
  if (typeof update.muteAll === "boolean")
194
200
  merged.muteAll = update.muteAll;
195
- const guard = (path) => {
196
- const [sec, field] = path;
201
+ for (const [sec, field] of SENSITIVE_FIELDS) {
197
202
  if (update[sec]?.[field] === MASKED) {
198
203
  merged[sec][field] = existing[sec]?.[field] ?? "";
199
204
  }
200
- };
201
- guard(["email", "pass"]);
202
- guard(["email", "clientSecret"]);
203
- guard(["email", "refreshToken"]);
204
- guard(["email", "accessToken"]);
205
- guard(["sms", "secretAccessKey"]);
206
- guard(["telegram", "token"]);
207
- guard(["discord", "webhookUrl"]);
208
- guard(["discord", "clientSecret"]);
209
- guard(["slack", "webhookUrl"]);
210
- guard(["slack", "botToken"]);
211
- guard(["slack", "clientSecret"]);
212
- guard(["teams", "webhookUrl"]);
205
+ }
213
206
  return merged;
214
207
  }
215
208
  const app = express();
@@ -562,7 +555,7 @@ app.post("/api/test/telegram", async (_req, res) => {
562
555
  if (r.ok)
563
556
  sent++;
564
557
  else
565
- errors.push(`${chatId}: ${r.status} ${await r.text()}`);
558
+ errors.push(`${chatId}: ${await telegramSendErrorHint(r)}`);
566
559
  }
567
560
  catch (err) {
568
561
  errors.push(`${chatId}: ${err instanceof Error ? err.message : String(err)}`);
@@ -1518,10 +1511,11 @@ async function sendNotification(message, priority, client) {
1518
1511
  headers: { "Content-Type": "application/json" },
1519
1512
  body: JSON.stringify(body),
1520
1513
  });
1521
- if (r.ok)
1514
+ if (r.ok) {
1522
1515
  sent++;
1523
- else
1524
- errors.push(`${chatId}: ${await r.text()}`);
1516
+ continue;
1517
+ }
1518
+ errors.push(`${chatId}: ${await telegramSendErrorHint(r)}`);
1525
1519
  }
1526
1520
  if (sent === 0 && errors.length)
1527
1521
  throw new Error(errors.join("; "));
@@ -2843,6 +2837,10 @@ function createMcpServer(clientId, sessionTag) {
2843
2837
  ? `\n\nReply with: @${sessionTag} <your answer>`
2844
2838
  : `\n\nReply to this message with your answer.`;
2845
2839
  for (const chatId of cfg.telegram.chatIds) {
2840
+ // A non-2xx Telegram response does NOT reject this fetch — only a
2841
+ // network-level failure does — so this used to silently do nothing
2842
+ // on e.g. a revoked token: no error, no log line, just a question
2843
+ // the user never saw and no clue why. Story #443: check r.ok too.
2846
2844
  await fetch(`https://api.telegram.org/bot${cfg.telegram.token}/sendMessage`, {
2847
2845
  method: "POST",
2848
2846
  headers: { "Content-Type": "application/json" },
@@ -2850,7 +2848,10 @@ function createMcpServer(clientId, sessionTag) {
2850
2848
  chat_id: chatId,
2851
2849
  text: `${askPrefix} ${question}${replyHint}`,
2852
2850
  }),
2853
- }).catch((err) => log("→", "ask:telegram", `ERROR: ${err}`, clientId));
2851
+ })
2852
+ .then(async (r) => { if (!r.ok)
2853
+ log("→", "ask:telegram", `ERROR: ${chatId}: ${await telegramSendErrorHint(r)}`, clientId); })
2854
+ .catch((err) => log("→", "ask:telegram", `ERROR: ${chatId}: ${err}`, clientId));
2854
2855
  }
2855
2856
  }
2856
2857
  const email = cfg.email ?? {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omni-notify-mcp",
3
- "version": "1.3.21",
3
+ "version": "1.3.23",
4
4
  "description": "Reach your AI agents from anywhere — and let them reach you. Every notification channel is set up in one config UI with one-click server launch: desktop, Telegram, Slack, SMS, email, ntfy. Agents then notify you when long work finishes or a decision is needed, ask you a question and wait for your answer, and receive and reply to the messages you send them — with Do Not Disturb and idle detection deciding what actually gets through.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",