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,
|
getNotes,
|
||||||
getNotesByDid,
|
getNotesByDid,
|
||||||
getNotesByDids,
|
getNotesByDids,
|
||||||
|
listAllWebhooks,
|
||||||
listWebhooksByDid,
|
listWebhooksByDid,
|
||||||
type WebhookVerb,
|
type WebhookVerb,
|
||||||
} from "./src/data/db.ts";
|
} from "./src/data/db.ts";
|
||||||
@@ -39,6 +40,32 @@ const requireDidOwnership = async (
|
|||||||
return true;
|
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 router = new Router();
|
||||||
|
|
||||||
const PAGINATION = 20;
|
const PAGINATION = 20;
|
||||||
@@ -117,6 +144,11 @@ router.post("/notes/feed", async (ctx) => {
|
|||||||
|
|
||||||
const ALLOWED_VERBS = ["create", "delete", "bulk-create"] as const;
|
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) => {
|
router.get("/:did/webhooks", async (ctx) => {
|
||||||
const { did } = ctx.params;
|
const { did } = ctx.params;
|
||||||
if (!(await requireDidOwnership(ctx, did))) return;
|
if (!(await requireDidOwnership(ctx, did))) return;
|
||||||
|
|||||||
@@ -136,6 +136,12 @@ export const listWebhooksByDid = (
|
|||||||
).all<Omit<WebhookSubscriptionRow, "token">>(did);
|
).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 = (
|
export const getWebhooksByDidAndVerb = (
|
||||||
did: string,
|
did: string,
|
||||||
verb: WebhookVerb,
|
verb: WebhookVerb,
|
||||||
|
|||||||
Reference in New Issue
Block a user