Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/public-search-suggestions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"emdash": patch
---

Fixes public LiveSearch autocomplete requests being blocked by auth middleware.
1 change: 1 addition & 0 deletions packages/core/src/astro/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ const PUBLIC_API_EXACT = new Set([
// so unauthenticated callers only see published content. Admin endpoints
// (/enable, /rebuild, /stats) remain private because they're not in this set.
"/_emdash/api/search",
"/_emdash/api/search/suggest",
]);

// Build merged public routes at module load from auth provider descriptors.
Expand Down
17 changes: 17 additions & 0 deletions packages/core/tests/integration/search/suggest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,23 @@ describe("getSuggestions (Integration)", () => {
});
});

it("does not return draft content", async () => {
await repo.create(
createPostFixture({
slug: "designing-secret-things",
status: "draft",
data: { title: "Designing secret things" },
}),
);

const suggestions = await getSuggestions(db, "des", {
collections: ["post"],
});

expect(suggestions).toHaveLength(1);
expect(suggestions[0]?.title).toBe("Designing things");
});

it("returns empty array for a non-matching query", async () => {
const suggestions = await getSuggestions(db, "zzz", {
collections: ["post"],
Expand Down
30 changes: 28 additions & 2 deletions packages/core/tests/unit/middleware/oauth-csrf.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ async function runAuthMiddleware(opts: {
extraHeaders?: Record<string, string>;
}): Promise<{ response: Response; next: ReturnType<typeof vi.fn> }> {
const url = new URL(opts.pathname, "https://site.example.com");
const method = opts.method ?? "POST";
const headers: Record<string, string> = {
"Content-Type": "application/json",
...opts.extraHeaders,
Expand All @@ -53,9 +54,9 @@ async function runAuthMiddleware(opts: {
{
url,
request: new Request(url, {
method: opts.method ?? "POST",
method,
headers,
body: "{}",
body: method === "GET" || method === "HEAD" ? undefined : "{}",
}),
locals: {
emdash: { db: {}, config: {} },
Expand All @@ -70,6 +71,31 @@ async function runAuthMiddleware(opts: {
return { response, next };
}

describe("Public search API routes", () => {
it.each(["/_emdash/api/search", "/_emdash/api/search/suggest"])(
"allows anonymous GET to %s",
async (pathname) => {
const { response, next } = await runAuthMiddleware({
pathname,
method: "GET",
});

expect(next).toHaveBeenCalledOnce();
expect(response.status).toBe(200);
},
);

it("keeps search management endpoints private", async () => {
const { response, next } = await runAuthMiddleware({
pathname: "/_emdash/api/search/rebuild",
method: "GET",
});

expect(next).not.toHaveBeenCalled();
expect(response.status).toBe(401);
});
});

/**
* OAuth protocol endpoints (RFC 6749, 7591, 8628) are designed to be called
* cross-origin. They must bypass the Origin-based CSRF check that applies to
Expand Down
Loading