From e7796b47c31134cf730728a8a52f08a0c8fb0049 Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Sun, 7 Jun 2026 11:26:14 +0200 Subject: [PATCH] feat(server): add /search and /:did/search endpoints GET /search is public and filters to discoverable=true. GET /:did/search requires a JWT for that DID and includes non-discoverable notes (drafts). Both reuse the existing PAGINATION default and { results, cursor } envelope. The requireDidOwnership 403 message is generalized since it now guards more than webhooks. --- server.ts | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/server.ts b/server.ts index 449bcbd..9550bb5 100644 --- a/server.ts +++ b/server.ts @@ -11,6 +11,7 @@ import { type WebhookVerb, } from "./src/data/db.ts"; import { authenticateRequest } from "./src/auth/verify.ts"; +import { searchNotes } from "./src/search/opensearch.ts"; import { log } from "./src/log.ts"; type AuthCtx = { @@ -34,7 +35,7 @@ const requireDidOwnership = async ( } if (verifiedDid !== did) { ctx.response.status = 403; - ctx.response.body = { error: "You can only manage your own webhooks" }; + ctx.response.body = { error: "Forbidden: DID mismatch" }; return false; } return true; @@ -131,6 +132,43 @@ router.get("/:did/notes", (ctx) => { ctx.response.body = getNotesByDid(did, cursor, limit); }); +router.get("/search", async (ctx) => { + const q = ctx.request.url.searchParams.get("q") ?? ""; + if (!q.trim()) { + ctx.response.status = 400; + ctx.response.body = { error: "q is required" }; + return; + } + const cursor = ctx.request.url.searchParams.get("cursor") ?? undefined; + const limit = Number(ctx.request.url.searchParams.get("limit")) || PAGINATION; + ctx.response.body = await searchNotes({ + q, + discoverableOnly: true, + cursor, + limit, + }); +}); + +router.get("/:did/search", async (ctx) => { + const { did } = ctx.params; + if (!(await requireDidOwnership(ctx, did))) return; + const q = ctx.request.url.searchParams.get("q") ?? ""; + if (!q.trim()) { + ctx.response.status = 400; + ctx.response.body = { error: "q is required" }; + return; + } + const cursor = ctx.request.url.searchParams.get("cursor") ?? undefined; + const limit = Number(ctx.request.url.searchParams.get("limit")) || PAGINATION; + ctx.response.body = await searchNotes({ + q, + discoverableOnly: false, + did, + cursor, + limit, + }); +}); + router.post("/notes/feed", async (ctx) => { const body = await ctx.request.body.json(); const { dids, cursor, limit } = body ?? {};