summaryrefslogtreecommitdiffhomepage
path: root/packages/api
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-01 11:28:03 +0900
committerAdam Malczewski <[email protected]>2026-06-01 11:28:03 +0900
commitd3d94b69a9f98a0a4b68dc6ee830466471adf26d (patch)
tree3320ab5b89d0ebb7578a43df28b6b95d2c3e0b3c /packages/api
parent07970bd4c89068272b76407beb6df5bc9aef2ff7 (diff)
downloaddispatch-d3d94b69a9f98a0a4b68dc6ee830466471adf26d.tar.gz
dispatch-d3d94b69a9f98a0a4b68dc6ee830466471adf26d.zip
feat(notifications): topic-only input (drop URL validation)
The Settings field is now a plain topic name (e.g. `my-secret-topic`) instead of a full URL. The transport always posts to `https://ntfy.sh/<topic>` (URL-encoded), and the only server-side check is "non-empty when enabled". Removes the user-visible "string does not match the expected pattern" error people hit when typing a bare topic. - packages/core/src/notifications/ntfy.ts: drop validateTopicUrl; add buildNtfyUrl(topic) + exported NTFY_BASE_URL. - packages/core/src/notifications/types.ts, config.ts: rename topicUrl -> topic; update docs. - packages/api/src/routes/notifications.ts: only validates non-empty topic when enabled. Also fixes a latent bug where notifySubagents was dropped on every PUT (was not passed to normalizeNtfyConfig). - packages/frontend/src/lib/components/SettingsPanel.svelte: relabel field "Topic URL" -> "Topic"; placeholder "your-secret-topic"; updated helper copy. - Tests updated: rewrote validateTopicUrl coverage as buildNtfyUrl coverage + proof that previously-rejected topics (dots, spaces, unicode, "Any Topic Whatsoever") now POST cleanly. - HANDOFF.md: added a short "topic-only input" section.
Diffstat (limited to 'packages/api')
-rw-r--r--packages/api/src/routes/notifications.ts21
-rw-r--r--packages/api/tests/routes.test.ts7
2 files changed, 16 insertions, 12 deletions
diff --git a/packages/api/src/routes/notifications.ts b/packages/api/src/routes/notifications.ts
index 57519bc..473e837 100644
--- a/packages/api/src/routes/notifications.ts
+++ b/packages/api/src/routes/notifications.ts
@@ -10,7 +10,6 @@ import {
redactNtfyConfig,
saveNtfyConfig,
sendNtfy,
- validateTopicUrl,
} from "@dispatch/core";
import { Hono } from "hono";
@@ -37,14 +36,21 @@ notificationsRoutes.put("/", async (c) => {
const merged = normalizeNtfyConfig({
enabled: typeof body.enabled === "boolean" ? body.enabled : existing.enabled,
- topicUrl: typeof body.topicUrl === "string" ? body.topicUrl : existing.topicUrl,
+ topic: typeof body.topic === "string" ? body.topic : existing.topic,
authToken: nextAuthToken,
events: { ...existing.events, ...(body.events ?? {}) },
+ notifySubagents:
+ typeof body.notifySubagents === "boolean" ? body.notifySubagents : existing.notifySubagents,
});
- if (merged.enabled) {
- const err = validateTopicUrl(merged.topicUrl);
- if (err) return c.json({ error: err }, 400);
+ // Only validation: if notifications are turned on, the topic must be
+ // non-empty. Any other "is this a valid ntfy topic name?" check is
+ // punted to the ntfy server itself — its rules vary and have changed
+ // over time, and a syntactically-valid name still might be rejected
+ // (e.g. reserved words), so a clear server error is more useful than
+ // a client-side guess.
+ if (merged.enabled && !merged.topic.trim()) {
+ return c.json({ error: "Topic is required" }, 400);
}
saveNtfyConfig(merged);
@@ -56,8 +62,9 @@ notificationsRoutes.post("/test", async (c) => {
if (!config.enabled) {
return c.json({ ok: false, error: "Notifications are disabled" }, 400);
}
- const err = validateTopicUrl(config.topicUrl);
- if (err) return c.json({ ok: false, error: err }, 400);
+ if (!config.topic.trim()) {
+ return c.json({ ok: false, error: "Topic is required" }, 400);
+ }
// Use a real event type so the per-event toggle is honored when wiring
// is tested end-to-end; pick `turn-completed` since it's the most
diff --git a/packages/api/tests/routes.test.ts b/packages/api/tests/routes.test.ts
index 1fad690..5606754 100644
--- a/packages/api/tests/routes.test.ts
+++ b/packages/api/tests/routes.test.ts
@@ -282,7 +282,7 @@ vi.mock("@dispatch/core", () => ({
loadNtfyConfig() {
return {
enabled: false,
- topicUrl: "",
+ topic: "",
authToken: "",
events: {
"turn-completed": true,
@@ -300,7 +300,7 @@ vi.mock("@dispatch/core", () => ({
defaultNtfyConfig() {
return {
enabled: false,
- topicUrl: "",
+ topic: "",
authToken: "",
events: {
"turn-completed": true,
@@ -318,9 +318,6 @@ vi.mock("@dispatch/core", () => ({
async sendNtfy() {
return { ok: true };
},
- validateTopicUrl() {
- return null;
- },
}));
const { app } = await import("../src/app.js");