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); +});