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.
This commit is contained in:
Julien Calixte
2026-06-07 11:26:14 +02:00
parent 937e16e602
commit e7796b47c3

View File

@@ -11,6 +11,7 @@ import {
type WebhookVerb, type WebhookVerb,
} from "./src/data/db.ts"; } from "./src/data/db.ts";
import { authenticateRequest } from "./src/auth/verify.ts"; import { authenticateRequest } from "./src/auth/verify.ts";
import { searchNotes } from "./src/search/opensearch.ts";
import { log } from "./src/log.ts"; import { log } from "./src/log.ts";
type AuthCtx = { type AuthCtx = {
@@ -34,7 +35,7 @@ const requireDidOwnership = async (
} }
if (verifiedDid !== did) { if (verifiedDid !== did) {
ctx.response.status = 403; 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 false;
} }
return true; return true;
@@ -131,6 +132,43 @@ router.get("/:did/notes", (ctx) => {
ctx.response.body = getNotesByDid(did, cursor, limit); 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) => { router.post("/notes/feed", async (ctx) => {
const body = await ctx.request.body.json(); const body = await ctx.request.body.json();
const { dids, cursor, limit } = body ?? {}; const { dids, cursor, limit } = body ?? {};