From 709c0c817a31303ade2fa54c5f3ef75a12cfb1bb Mon Sep 17 00:00:00 2001 From: Jakob Wennberg Date: Tue, 11 Aug 2026 14:01:00 +0200 Subject: [PATCH] fix(auth): serve /docs and llms.txt to anonymous agents (#1520) The middleware allowlist never included the agent-discovery surfaces, so every anonymous request to /llms.txt, /llms-full.txt, or /docs/* was 307-bounced to /login on hosted. The llms.txt convention exists for logged-out crawlers and IDE agents, and /docs is the public API documentation that the OpenAPI spec and the installable accounted-api skill link to. openapi.json and /.well-known/* only escaped because the proxy matcher skips .json and .well-known paths. Signed-in users fall through to the same content with no redirect. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 --- lib/supabase/__tests__/middleware.test.ts | 26 +++++++++++++++++++++++ lib/supabase/middleware.ts | 17 +++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/lib/supabase/__tests__/middleware.test.ts b/lib/supabase/__tests__/middleware.test.ts index c755a9a4..fdb7b484 100644 --- a/lib/supabase/__tests__/middleware.test.ts +++ b/lib/supabase/__tests__/middleware.test.ts @@ -249,6 +249,32 @@ describe('updateSession redirect destinations', () => { }) }) + // ── Public agent-discovery + docs surfaces ──────────────────────────── + + describe('anonymous access to agent-discovery and docs surfaces', () => { + it.each(['/llms.txt', '/llms-full.txt', '/docs/api', '/docs/api.md', '/docs/api/reference.md', '/docs/api/cookbook/quickstart.md'])( + 'serves %s without a login bounce', + async (path) => { + const response = await run(path) + expect(response.status).not.toBe(307) + expect(locationOf(response)).toBeNull() + }, + ) + + it('does not treat a /docs prefix on another route as public', async () => { + // /docsy-dashboard must still bounce: only /docs and /docs/* are public. + const response = await run('/docsy-dashboard') + expect(response.status).toBe(307) + expect(new URL(locationOf(response)!).pathname).toBe('/login') + }) + + it('serves docs to a signed-in user without redirecting away', async () => { + state.user = SIGNED_IN + const response = await run('/docs/api') + expect(response.status).not.toBe(307) + }) + }) + // ── Site 1: protected-route bounce ──────────────────────────────────── describe('protected route bounce to /login', () => { diff --git a/lib/supabase/middleware.ts b/lib/supabase/middleware.ts index 5b638f30..99b528c3 100644 --- a/lib/supabase/middleware.ts +++ b/lib/supabase/middleware.ts @@ -201,6 +201,23 @@ export async function updateSession(request: NextRequest) { return supabaseResponse } + // Public agent-discovery + API docs surfaces. /llms.txt and /llms-full.txt + // exist FOR anonymous consumers (the llms.txt convention targets logged-out + // crawlers and IDE agents), and /docs is the public API documentation the + // OpenAPI spec and the installable accounted-api skill link to. None of it + // reads the session. Without this branch every anonymous hit 307-bounced to + // /login, which silently broke agent discovery on the hosted product + // (openapi.json only escaped because the proxy matcher skips .json paths). + // Logged-in users fall through to the same content: no redirect either way. + if ( + pathname === '/llms.txt' || + pathname === '/llms-full.txt' || + pathname === '/docs' || + pathname.startsWith('/docs/') + ) { + return supabaseResponse + } + // Public auth routes: allow access if ( pathname.startsWith('/login') ||