From 34faa10be2ea082f45730adc7862bb41918ca35e Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Tue, 5 May 2026 14:07:22 +0200 Subject: [PATCH] feat(webhooks): add admin endpoint to list every subscription MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds GET /admin/webhooks gated by an ADMIN_DIDS env-var allowlist of verified AT Proto DIDs. Fail-closed: if ADMIN_DIDS is unset, the route always returns 403 — no accidental exposure on deploys that forget it. --- server.ts | 32 ++++++++++++++++++++++++++++++++ src/data/db.ts | 6 ++++++ 2 files changed, 38 insertions(+) diff --git a/server.ts b/server.ts index ab8ea49..449bcbd 100644 --- a/server.ts +++ b/server.ts @@ -6,6 +6,7 @@ import { getNotes, getNotesByDid, getNotesByDids, + listAllWebhooks, listWebhooksByDid, type WebhookVerb, } from "./src/data/db.ts"; @@ -39,6 +40,32 @@ const requireDidOwnership = async ( return true; }; +const ADMIN_DIDS = new Set( + (Deno.env.get("ADMIN_DIDS") ?? "") + .split(",") + .map((d) => d.trim()) + .filter(Boolean), +); + +const requireAdmin = async (ctx: AuthCtx): Promise => { + let verifiedDid: string; + try { + verifiedDid = await authenticateRequest( + ctx.request.headers.get("Authorization"), + ); + } catch { + ctx.response.status = 401; + ctx.response.body = { error: "Unauthorized" }; + return false; + } + if (!ADMIN_DIDS.has(verifiedDid)) { + ctx.response.status = 403; + ctx.response.body = { error: "Admin only" }; + return false; + } + return true; +}; + const router = new Router(); const PAGINATION = 20; @@ -117,6 +144,11 @@ router.post("/notes/feed", async (ctx) => { const ALLOWED_VERBS = ["create", "delete", "bulk-create"] as const; +router.get("/admin/webhooks", async (ctx) => { + if (!(await requireAdmin(ctx))) return; + ctx.response.body = listAllWebhooks(); +}); + router.get("/:did/webhooks", async (ctx) => { const { did } = ctx.params; if (!(await requireDidOwnership(ctx, did))) return; diff --git a/src/data/db.ts b/src/data/db.ts index e62fc3e..863b2c7 100644 --- a/src/data/db.ts +++ b/src/data/db.ts @@ -136,6 +136,12 @@ export const listWebhooksByDid = ( ).all>(did); }; +export const listAllWebhooks = (): Omit[] => { + return db.prepare( + "SELECT id, did, method, url, verb FROM webhook_subscription ORDER BY did, id", + ).all>(); +}; + export const getWebhooksByDidAndVerb = ( did: string, verb: WebhookVerb,