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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
bf220eac3c
commit
709c0c817a
@@ -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 ────────────────────────────────────
|
// ── Site 1: protected-route bounce ────────────────────────────────────
|
||||||
|
|
||||||
describe('protected route bounce to /login', () => {
|
describe('protected route bounce to /login', () => {
|
||||||
|
|||||||
@@ -201,6 +201,23 @@ export async function updateSession(request: NextRequest) {
|
|||||||
return supabaseResponse
|
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
|
// Public auth routes: allow access
|
||||||
if (
|
if (
|
||||||
pathname.startsWith('/login') ||
|
pathname.startsWith('/login') ||
|
||||||
|
|||||||
Reference in New Issue
Block a user