feat(webhooks): add admin endpoint to list every subscription
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.
This commit is contained in:
32
server.ts
32
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<boolean> => {
|
||||
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;
|
||||
|
||||
@@ -136,6 +136,12 @@ export const listWebhooksByDid = (
|
||||
).all<Omit<WebhookSubscriptionRow, "token">>(did);
|
||||
};
|
||||
|
||||
export const listAllWebhooks = (): Omit<WebhookSubscriptionRow, "token">[] => {
|
||||
return db.prepare(
|
||||
"SELECT id, did, method, url, verb FROM webhook_subscription ORDER BY did, id",
|
||||
).all<Omit<WebhookSubscriptionRow, "token">>();
|
||||
};
|
||||
|
||||
export const getWebhooksByDidAndVerb = (
|
||||
did: string,
|
||||
verb: WebhookVerb,
|
||||
|
||||
Reference in New Issue
Block a user