diff --git a/app/api/domain-webhooks/route.ts b/app/api/domain-webhooks/route.ts
index ee77ca8..c04f7c9 100644
--- a/app/api/domain-webhooks/route.ts
+++ b/app/api/domain-webhooks/route.ts
@@ -7,6 +7,7 @@ import {
listDomainWebhooks,
addDomainWebhook,
deleteDomainWebhook,
+ setDomainWebhookActive,
listInboundEvents,
distinctWebhookUrls,
} from "@/lib/db";
@@ -58,7 +59,7 @@ export async function GET(req: NextRequest) {
});
}
-// POST /api/domain-webhooks — owner: add a target, delete one, or send a test.
+// POST /api/domain-webhooks — owner: add, pause/resume, delete, or test a target.
export async function POST(req: NextRequest) {
const accountId = await resolveAccountId(req);
if (!accountId) return NextResponse.json({ error: "Sign in to manage webhooks." }, { status: 401 });
@@ -74,6 +75,16 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ ok }, { status: ok ? 200 : 404 });
}
+ if (body.action === "setActive") {
+ const id = String(body.id || "");
+ if (!id || typeof body.active !== "boolean") {
+ return NextResponse.json({ error: "Webhook id and active state are required." }, { status: 400 });
+ }
+ const ok = await setDomainWebhookActive(id, dn, body.active);
+ if (!ok) return NextResponse.json({ error: "Webhook target not found." }, { status: 404 });
+ return NextResponse.json({ ok: true, id, active: body.active });
+ }
+
if (body.action === "test") {
await fireDomainEvent(dn, "test.ping", { message: "Test event from moshcoding", dn });
return NextResponse.json({ ok: true, sent: true });
diff --git a/app/dashboard/[[...tab]]/page.tsx b/app/dashboard/[[...tab]]/page.tsx
index fa14f59..b3683e1 100644
--- a/app/dashboard/[[...tab]]/page.tsx
+++ b/app/dashboard/[[...tab]]/page.tsx
@@ -905,6 +905,7 @@ function DomainWebhooksPanel({ onError, onOk }: { onError: (m: string) => void;
Outbound targets{data.knownUrls?.length ? · pre-filled from your project webhooks : null}
+ Pause a target to stop deliveries without deleting its signing secret.
setUrl(e.target.value)} />
{data.knownUrls?.length ? (
@@ -917,9 +918,19 @@ function DomainWebhooksPanel({ onError, onOk }: { onError: (m: string) => void;
{(!data.webhooks || data.webhooks.length === 0) &&
No outbound targets yet.}
{data.webhooks?.map((w: any) => (
- {w.url} · secret {String(w.secret).slice(0, 12)}…
+ {w.url} · {w.active ? "active" : "paused"} · secret {String(w.secret).slice(0, 12)}…
+
diff --git a/lib/db.ts b/lib/db.ts
index 9116edb..4f3b4fc 100644
--- a/lib/db.ts
+++ b/lib/db.ts
@@ -909,6 +909,16 @@ export async function deleteDomainWebhook(id: string, dn: string): Promise 0;
}
+/** Pause or resume one outbound target without discarding its signing secret. */
+export async function setDomainWebhookActive(id: string, dn: string, active: boolean): Promise {
+ await ensureSchema();
+ const r = await db().execute({
+ sql: `UPDATE domain_webhooks SET active = ? WHERE id = ? AND dn = ?`,
+ args: [active ? 1 : 0, id, dn.toLowerCase()],
+ });
+ return Number(r.rowsAffected || 0) > 0;
+}
+
/** Stores an inbound event, keeping only the most recent 200 per domain. */
export async function recordInboundEvent(opts: { dn: string; source?: string | null; eventType?: string | null; payload: string }): Promise {
await ensureSchema();
diff --git a/tests/domain-webhook-active.test.mjs b/tests/domain-webhook-active.test.mjs
new file mode 100644
index 0000000..23aa310
--- /dev/null
+++ b/tests/domain-webhook-active.test.mjs
@@ -0,0 +1,39 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+process.env.TURSO_DATABASE_URL = "file::memory:";
+
+const {
+ activeDomainWebhooks,
+ addDomainWebhook,
+ listDomainWebhooks,
+ setDomainWebhookActive,
+} = await import("../lib/db.ts");
+
+test("domain webhook targets can pause and resume without losing their configuration", async () => {
+ const webhook = await addDomainWebhook("example.test", "https://hooks.example.test/events", "whsec_keep_me");
+
+ assert.equal(webhook.active, true);
+ assert.deepEqual(await activeDomainWebhooks("example.test"), [
+ { url: "https://hooks.example.test/events", secret: "whsec_keep_me" },
+ ]);
+
+ assert.equal(await setDomainWebhookActive(webhook.id, "example.test", false), true);
+ assert.deepEqual(await activeDomainWebhooks("example.test"), []);
+ assert.deepEqual(
+ (await listDomainWebhooks("example.test")).map(({ url, secret, active }) => ({ url, secret, active })),
+ [{ url: "https://hooks.example.test/events", secret: "whsec_keep_me", active: false }],
+ );
+
+ assert.equal(await setDomainWebhookActive(webhook.id, "example.test", true), true);
+ assert.deepEqual(await activeDomainWebhooks("example.test"), [
+ { url: "https://hooks.example.test/events", secret: "whsec_keep_me" },
+ ]);
+});
+
+test("a target cannot be changed through another domain", async () => {
+ const webhook = await addDomainWebhook("owner.test", "https://hooks.example.test/owner", "whsec_owner");
+
+ assert.equal(await setDomainWebhookActive(webhook.id, "other.test", false), false);
+ assert.equal((await listDomainWebhooks("owner.test"))[0].active, true);
+});