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 {