diff options
| author | Adam Malczewski <[email protected]> | 2026-06-01 11:28:03 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-01 11:28:03 +0900 |
| commit | d3d94b69a9f98a0a4b68dc6ee830466471adf26d (patch) | |
| tree | 3320ab5b89d0ebb7578a43df28b6b95d2c3e0b3c /packages/api | |
| parent | 07970bd4c89068272b76407beb6df5bc9aef2ff7 (diff) | |
| download | dispatch-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.ts | 21 | ||||
| -rw-r--r-- | packages/api/tests/routes.test.ts | 7 |
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"); |
