From 424ef6304c1d3f0ccbf767ef73125ff329adb469 Mon Sep 17 00:00:00 2001 From: Cesar Sacconi Falcao Date: Thu, 9 Apr 2026 22:14:43 -0400 Subject: [PATCH] fix(middleware): allow public access to search API at middleware layer PR #107 removed the handler-level `requirePerm()` check from the search endpoints, but the auth middleware still returned 401 before the handlers ran because `/_emdash/api/search` was not in `PUBLIC_API_EXACT`. The handler-level changes therefore never executed for anonymous callers, and the shipped `LiveSearch` component (which fetches without credentials) silently showed "No results found" on every query. This change adds `/_emdash/api/search` to `PUBLIC_API_EXACT` so the middleware lets anonymous GET requests reach the handler. The query layer already hardcodes `status='published'`, so anonymous callers still only see published content. Admin endpoints (`/enable`, `/rebuild`, `/stats`, and `/suggest`) remain authenticated because they are not in the set. The existing E2E test `"search endpoint requires authentication"` asserted the buggy behavior and is replaced with two new tests: one verifying public access to `/_emdash/api/search`, and one verifying that `/stats` and `/enable` remain gated. Closes #104. Follows up on #107. Co-Authored-By: Claude Opus 4.6 (1M context) --- .changeset/public-search-middleware.md | 5 +++++ e2e/tests/search.spec.ts | 23 ++++++++++++++++++---- packages/core/src/astro/middleware/auth.ts | 4 ++++ 3 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 .changeset/public-search-middleware.md diff --git a/.changeset/public-search-middleware.md b/.changeset/public-search-middleware.md new file mode 100644 index 00000000..e5733d15 --- /dev/null +++ b/.changeset/public-search-middleware.md @@ -0,0 +1,5 @@ +--- +"emdash": patch +--- + +Fixes public access to the search API (#104). The auth middleware blocked `/_emdash/api/search` before the handler ran, so #107's handler-level change never took effect for anonymous callers. Adds the endpoint to `PUBLIC_API_EXACT` so the shipped `LiveSearch` component works on public sites without credentials. Admin endpoints (`/search/enable`, `/search/rebuild`, `/search/stats`, `/search/suggest`) remain authenticated. diff --git a/e2e/tests/search.spec.ts b/e2e/tests/search.spec.ts index 9255a08b..81c87701 100644 --- a/e2e/tests/search.spec.ts +++ b/e2e/tests/search.spec.ts @@ -186,11 +186,26 @@ test.describe("Search", () => { }); test.describe("Search API", () => { - test("search endpoint requires authentication", async ({ serverInfo }) => { - // Request without auth token + test("search endpoint is publicly accessible", async ({ serverInfo }) => { + // The LiveSearch component is shipped for public-site use and calls this + // endpoint without credentials. The query layer hardcodes status='published', + // so anonymous callers can only see published content. const res = await fetch(`${serverInfo.baseUrl}/_emdash/api/search?q=test`); - // Should be 401 or 403 - expect([401, 403]).toContain(res.status); + expect(res.status).toBe(200); + }); + + test("search admin endpoints still require authentication", async ({ serverInfo }) => { + // Admin-only: enable, rebuild, stats must stay gated even though the + // read endpoint is public. + const stats = await fetch(`${serverInfo.baseUrl}/_emdash/api/search/stats`); + expect([401, 403]).toContain(stats.status); + + const enable = await fetch(`${serverInfo.baseUrl}/_emdash/api/search/enable`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ collection: "posts" }), + }); + expect([401, 403]).toContain(enable.status); }); test("search endpoint requires a query parameter", async ({ serverInfo }) => { diff --git a/packages/core/src/astro/middleware/auth.ts b/packages/core/src/astro/middleware/auth.ts index 76228d1c..573caa24 100644 --- a/packages/core/src/astro/middleware/auth.ts +++ b/packages/core/src/astro/middleware/auth.ts @@ -108,6 +108,10 @@ const PUBLIC_API_EXACT = new Set([ "/_emdash/api/auth/passkey/verify", "/_emdash/api/oauth/token", "/_emdash/api/snapshot", + // Public site search — read-only. The query layer hardcodes status='published' + // so unauthenticated callers only see published content. Admin endpoints + // (/enable, /rebuild, /stats) remain private because they're not in this set. + "/_emdash/api/search", ]); function isPublicEmDashRoute(pathname: string): boolean {