diff --git a/src/app/api/documents/[id]/route.ts b/src/app/api/documents/[id]/route.ts index 55d6a1558..4f404a004 100644 --- a/src/app/api/documents/[id]/route.ts +++ b/src/app/api/documents/[id]/route.ts @@ -8,6 +8,7 @@ import { invalidateRagCachesForDocumentMutation } from "@/lib/rag"; import { committedIndexGeneration, isCommittedGenerationMetadata } from "@/lib/reindex-pipeline"; import { createAdminClient } from "@/lib/supabase/admin"; import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth"; +import { publicAccessContext, withOwnerReadScope } from "@/lib/public-api-access"; import { writeAuditLog } from "@/lib/audit"; import { parseJsonBody } from "@/lib/validation/body"; import { parseRouteParams } from "@/lib/validation/params"; @@ -280,13 +281,11 @@ export async function GET(request: Request, { params }: { params: Promise<{ id: const { id } = parseRouteParams({ id: rawId }, documentRouteParamsSchema, "Invalid document id."); const supabase = createAdminClient(); - const user = await requireAuthenticatedUser(request, supabase); - const { data: document, error } = await supabase - .from("documents") - .select("*") - .eq("id", id) - .eq("owner_id", user.id) - .maybeSingle(); + const access = await publicAccessContext(request, supabase); + const { data: document, error } = await withOwnerReadScope( + supabase.from("documents").select("*").eq("id", id), + access.ownerId, + ).maybeSingle(); if (error) throw new Error(error.message); if (!document) return NextResponse.json({ error: "Document not found." }, { status: 404 }); diff --git a/src/app/api/documents/[id]/search/route.ts b/src/app/api/documents/[id]/search/route.ts index 4ab60b1ed..d153f9859 100644 --- a/src/app/api/documents/[id]/search/route.ts +++ b/src/app/api/documents/[id]/search/route.ts @@ -5,7 +5,8 @@ import { isDemoMode } from "@/lib/env"; import { jsonError } from "@/lib/http"; import { committedIndexGeneration, isCommittedGenerationMetadata } from "@/lib/reindex-pipeline"; import { createAdminClient } from "@/lib/supabase/admin"; -import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth"; +import { AuthenticationError, unauthorizedResponse } from "@/lib/supabase/auth"; +import { publicAccessContext, withOwnerReadScope } from "@/lib/public-api-access"; import { parseRouteParams } from "@/lib/validation/params"; import { parseRequestQuery, queryInteger } from "@/lib/validation/query"; @@ -181,13 +182,11 @@ export async function GET(request: Request, { params }: { params: Promise<{ id: const { id } = parseRouteParams({ id: rawId }, documentSearchParamsSchema, "Invalid document id."); const supabase = createAdminClient(); - const user = await requireAuthenticatedUser(request, supabase); - const { data: document, error: documentError } = await supabase - .from("documents") - .select("id,metadata") - .eq("id", id) - .eq("owner_id", user.id) - .maybeSingle(); + const access = await publicAccessContext(request, supabase); + const { data: document, error: documentError } = await withOwnerReadScope( + supabase.from("documents").select("id,metadata").eq("id", id), + access.ownerId, + ).maybeSingle(); if (documentError) throw new Error(documentError.message); if (!document) return NextResponse.json({ error: "Document not found." }, { status: 404 }); @@ -197,7 +196,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ id: p_document_id: id, p_query: query, match_count: limit, - p_owner_id: user.id, + p_owner_id: access.ownerId, }); if (!rpcError) { diff --git a/src/app/api/documents/[id]/signed-url/route.ts b/src/app/api/documents/[id]/signed-url/route.ts index 043d01118..5dd6dc8ea 100644 --- a/src/app/api/documents/[id]/signed-url/route.ts +++ b/src/app/api/documents/[id]/signed-url/route.ts @@ -5,7 +5,8 @@ import { env } from "@/lib/env"; import { isDemoMode } from "@/lib/env"; import { jsonError, PublicApiError } from "@/lib/http"; import { createAdminClient } from "@/lib/supabase/admin"; -import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth"; +import { AuthenticationError, unauthorizedResponse } from "@/lib/supabase/auth"; +import { publicAccessContext, withOwnerReadScope } from "@/lib/public-api-access"; export const runtime = "nodejs"; @@ -31,13 +32,11 @@ export async function GET(_request: Request, { params }: { params: Promise<{ id: if (!routeIdSchema.safeParse(id).success) throw new PublicApiError("Invalid document id."); const supabase = createAdminClient(); - const user = await requireAuthenticatedUser(_request, supabase); - const { data: document, error } = await supabase - .from("documents") - .select("storage_path,file_type") - .eq("id", id) - .eq("owner_id", user.id) - .maybeSingle(); + const access = await publicAccessContext(_request, supabase); + const { data: document, error } = await withOwnerReadScope( + supabase.from("documents").select("storage_path,file_type").eq("id", id), + access.ownerId, + ).maybeSingle(); if (error) throw new Error(error.message); if (!document) return NextResponse.json({ error: "Document not found." }, { status: 404 }); diff --git a/src/app/api/documents/route.ts b/src/app/api/documents/route.ts index 52814c3a6..8fa0958be 100644 --- a/src/app/api/documents/route.ts +++ b/src/app/api/documents/route.ts @@ -4,7 +4,8 @@ import { demoDocuments } from "@/lib/demo-data"; import { isDemoMode } from "@/lib/env"; import { jsonError } from "@/lib/http"; import { createAdminClient } from "@/lib/supabase/admin"; -import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth"; +import { AuthenticationError, unauthorizedResponse } from "@/lib/supabase/auth"; +import { publicAccessContext, withOwnerReadScope } from "@/lib/public-api-access"; import { parseRequestQuery, queryBoolean, queryInteger } from "@/lib/validation/query"; export const runtime = "nodejs"; @@ -130,11 +131,11 @@ export async function GET(request: Request) { } = parseRequestQuery(request, documentListQuerySchema, "Invalid document list query."); const supabase = createAdminClient(); - const user = await requireAuthenticatedUser(request, supabase); - let query = supabase - .from("documents") - .select(DOCUMENT_LIST_COLUMNS, { count: "exact" }) - .eq("owner_id", user.id) + const access = await publicAccessContext(request, supabase); + let query = withOwnerReadScope( + supabase.from("documents").select(DOCUMENT_LIST_COLUMNS, { count: "exact" }), + access.ownerId, + ) .order("created_at", { ascending: false }) .range(offset, offset + limit - 1); diff --git a/src/app/api/images/[id]/signed-url/route.ts b/src/app/api/images/[id]/signed-url/route.ts index b061aaf8a..9ca925d49 100644 --- a/src/app/api/images/[id]/signed-url/route.ts +++ b/src/app/api/images/[id]/signed-url/route.ts @@ -6,7 +6,8 @@ import { isDemoMode } from "@/lib/env"; import { jsonError, PublicApiError } from "@/lib/http"; import { committedIndexGeneration, isCommittedGenerationMetadata } from "@/lib/reindex-pipeline"; import { createAdminClient } from "@/lib/supabase/admin"; -import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth"; +import { AuthenticationError, unauthorizedResponse } from "@/lib/supabase/auth"; +import { publicAccessContext, withOwnerReadScope } from "@/lib/public-api-access"; export const runtime = "nodejs"; @@ -31,7 +32,7 @@ export async function GET(_request: Request, { params }: { params: Promise<{ id: if (!routeIdSchema.safeParse(id).success) throw new PublicApiError("Invalid image id."); const supabase = createAdminClient(); - const user = await requireAuthenticatedUser(_request, supabase); + const access = await publicAccessContext(_request, supabase); const { data: image, error } = await supabase .from("document_images") .select("document_id,storage_path,mime_type,caption,metadata") @@ -41,12 +42,10 @@ export async function GET(_request: Request, { params }: { params: Promise<{ id: if (error) throw new Error(error.message); if (!image) return NextResponse.json({ error: "Image not found." }, { status: 404 }); - const { data: document, error: documentError } = await supabase - .from("documents") - .select("id,metadata") - .eq("id", image.document_id) - .eq("owner_id", user.id) - .maybeSingle(); + const { data: document, error: documentError } = await withOwnerReadScope( + supabase.from("documents").select("id,metadata").eq("id", image.document_id), + access.ownerId, + ).maybeSingle(); if (documentError) throw new Error(documentError.message); if (!document) return NextResponse.json({ error: "Image not found." }, { status: 404 }); diff --git a/src/app/api/registry/records/[slug]/route.ts b/src/app/api/registry/records/[slug]/route.ts index bfa21c873..a966ab37a 100644 --- a/src/app/api/registry/records/[slug]/route.ts +++ b/src/app/api/registry/records/[slug]/route.ts @@ -4,7 +4,7 @@ import { z } from "zod"; import { consumeSubjectApiRateLimit, rateLimitJsonResponse } from "@/lib/api-rate-limit"; import { isDemoMode, isLocalNoAuthMode } from "@/lib/env"; import { jsonError } from "@/lib/http"; -import { publicAccessContext } from "@/lib/public-api-access"; +import { hasPublicApiAuthSignal, publicAccessContext } from "@/lib/public-api-access"; import { getFormRecord } from "@/lib/forms"; import { deriveGovernanceColumns, @@ -62,6 +62,15 @@ export async function GET(request: Request, context: { params: Promise<{ slug: s }); } + if (!hasPublicApiAuthSignal(request)) { + const payload = publicRegistryDetailPayload(kind, normalizedSlug); + if (!payload) return notFoundResponse(normalizedSlug); + return registryResponse({ + ...payload, + publicAccess: true, + }); + } + const supabase = createAdminClient(); const access = await publicAccessContext(request, supabase); diff --git a/src/app/api/registry/records/route.ts b/src/app/api/registry/records/route.ts index 5897a1525..ab07badfb 100644 --- a/src/app/api/registry/records/route.ts +++ b/src/app/api/registry/records/route.ts @@ -4,7 +4,7 @@ import { z } from "zod"; import { consumeSubjectApiRateLimit, rateLimitJsonResponse } from "@/lib/api-rate-limit"; import { isDemoMode, isLocalNoAuthMode } from "@/lib/env"; import { jsonError } from "@/lib/http"; -import { publicAccessContext } from "@/lib/public-api-access"; +import { hasPublicApiAuthSignal, publicAccessContext } from "@/lib/public-api-access"; import { rankFormRecords, formRecords } from "@/lib/forms"; import { deriveGovernanceColumns, @@ -78,6 +78,13 @@ export async function GET(request: Request) { }); } + if (!hasPublicApiAuthSignal(request)) { + return registryResponse({ + ...publicRegistryPayload(kind, q, limit), + publicAccess: true, + }); + } + const supabase = createAdminClient(); const access = await publicAccessContext(request, supabase); diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx index bd4c4d222..8d875334f 100644 --- a/src/components/DocumentViewer.tsx +++ b/src/components/DocumentViewer.tsx @@ -1914,7 +1914,6 @@ export function DocumentViewer({ const [documentSearchError, setDocumentSearchError] = useState(null); const [reviewingTableFactId, setReviewingTableFactId] = useState(null); const [isOnline, setIsOnline] = useState(true); - const [authLoadingTimedOut, setAuthLoadingTimedOut] = useState(false); const [localProjectReady, setLocalProjectReady] = useState(true); const [mobileActionsOpen, setMobileActionsOpen] = useState(false); const [useNativePdfViewer, setUseNativePdfViewer] = useState(() => getInitialPdfViewerMode().useNativePdfViewer); @@ -1927,6 +1926,7 @@ export function DocumentViewer({ const [serverDemoMode, setServerDemoMode] = useState(process.env.NEXT_PUBLIC_DEMO_MODE === "true"); const localNoAuthMode = isLocalNoAuthMode(); const clientDemoMode = localNoAuthMode || serverDemoMode; + const canViewSourceDocuments = localProjectReady; const canUsePrivateApis = localProjectReady && (clientDemoMode || authStatus === "authenticated"); useEffect(() => { @@ -2002,10 +2002,10 @@ export function DocumentViewer({ }, [isConfigured]); useEffect(() => { - if (!canUsePrivateApis && authStatus === "loading") { + if (!canViewSourceDocuments && authStatus === "loading") { return () => undefined; } - if (!canUsePrivateApis) { + if (!canViewSourceDocuments) { return () => undefined; } @@ -2141,7 +2141,7 @@ export function DocumentViewer({ }, [ authStatus, authorizationHeader, - canUsePrivateApis, + canViewSourceDocuments, clientDemoMode, documentId, chunkId, @@ -2208,15 +2208,6 @@ export function DocumentViewer({ }; }, []); - useEffect(() => { - if (canUsePrivateApis || authStatus !== "loading") { - return () => undefined; - } - - const timeout = window.setTimeout(() => setAuthLoadingTimedOut(true), 3000); - return () => window.clearTimeout(timeout); - }, [authStatus, canUsePrivateApis]); - async function summarize() { if (!canSummarizeDocument) { setSummaryError("Load a source document before summarising."); @@ -2247,15 +2238,8 @@ export function DocumentViewer({ } } - const authViewerError = - !canUsePrivateApis && (authStatus !== "loading" || authLoadingTimedOut) - ? isConfigured - ? "Sign in to open private source documents." - : "Supabase browser authentication is not configured for private source documents." - : null; - const effectiveLoadingDocument = !canUsePrivateApis - ? authStatus === "loading" && !authLoadingTimedOut && loadingDocument - : loadingDocument; + const authViewerError = null; + const effectiveLoadingDocument = loadingDocument; const effectiveViewerError = authViewerError ?? viewerError; const viewerState = effectiveLoadingDocument ? "loading" diff --git a/src/lib/public-api-access.ts b/src/lib/public-api-access.ts index 26b80f3fc..8ae3473e1 100644 --- a/src/lib/public-api-access.ts +++ b/src/lib/public-api-access.ts @@ -33,6 +33,17 @@ export function hasPublicApiAuthSignal(request: Request) { return cookieHeader.includes("sb-"); } +type OwnerScopedQuery = { + eq(column: string, value: unknown): T; + is(column: string, value: null): T; +}; + +/** Scope document reads to the authenticated owner or public (owner_id IS NULL) rows. */ +export function withOwnerReadScope>(query: T, ownerId: string | undefined): T { + if (ownerId) return query.eq("owner_id", ownerId); + return query.is("owner_id", null); +} + export async function publicAccessContext(request: Request, supabase: AdminClient) { const user = await getOptionalAuthenticatedUser(request, supabase); if (user) { diff --git a/src/lib/supabase/auth.ts b/src/lib/supabase/auth.ts index db67f2cbe..044384b07 100644 --- a/src/lib/supabase/auth.ts +++ b/src/lib/supabase/auth.ts @@ -128,10 +128,10 @@ export async function getOptionalAuthenticatedUser( const token = extractSessionAccessToken(request); if (token) { const { data, error } = await supabase.auth.getUser(token); - if (error || !data.user?.id) { - throw new AuthenticationError(); + if (!error && data.user?.id) { + return { id: data.user.id }; } - return { id: data.user.id }; + // Invalid or expired Bearer token: fall through to cookie session, then anonymous. } return getUserFromRequestCookies(request); diff --git a/tests/api-validation-contract.test.ts b/tests/api-validation-contract.test.ts index 4bdbf8834..5d68487d8 100644 --- a/tests/api-validation-contract.test.ts +++ b/tests/api-validation-contract.test.ts @@ -180,6 +180,13 @@ function mockRuntime(client: ReturnType) { vi.doMock("@/lib/supabase/auth", () => ({ AuthenticationError: class AuthenticationError extends Error {}, requireAuthenticatedUser, + getOptionalAuthenticatedUser: vi.fn(async (request: Request) => { + const authorization = request.headers.get("authorization") ?? ""; + if (/^Bearer\s+\S+/i.test(authorization)) return { id: userId }; + const cookieHeader = request.headers.get("cookie") ?? ""; + if (cookieHeader.includes("sb-")) return { id: userId }; + return null; + }), unauthorizedResponse: () => new Response(JSON.stringify({ error: "Authentication required." }), { status: 401, diff --git a/tests/private-access-routes.test.ts b/tests/private-access-routes.test.ts index 45b5b1e61..9d91205f7 100644 --- a/tests/private-access-routes.test.ts +++ b/tests/private-access-routes.test.ts @@ -376,14 +376,18 @@ afterEach(() => { }); describe("private document API access", () => { - it("still requires a valid token even when local no-auth mode is enabled", async () => { - const client = createSupabaseMock(); + it("lists public documents without auth in local no-auth mode", async () => { + const documents = [{ id: documentId, owner_id: null, title: "Public guideline" }]; + const client = createSupabaseMock((call) => (call.table === "documents" ? ok(documents) : ok([]))); mockRuntime(client, undefined, { localNoAuth: true }); const { GET } = await import("../src/app/api/documents/route"); const response = await GET(localPortRequest(4298, "/api/documents")); + const body = await payload(response); - expect(response.status).toBe(401); + expect(response.status).toBe(200); + expect(body.documents).toEqual(documents.map((document) => ({ ...document, labels: [], summary: null }))); + expect(client.calls[0].filters).toContainEqual({ column: "owner_id", value: null }); expect(client.auth.getUser).not.toHaveBeenCalled(); }); @@ -398,28 +402,39 @@ describe("private document API access", () => { expect(response.status).toBe(200); expect(client.auth.getUser).toHaveBeenCalledTimes(1); }); - it("rejects local no-auth private calls from unmanaged localhost ports before Supabase access", async () => { + it("rejects local no-auth upload calls from unmanaged localhost ports before Supabase access", async () => { const client = createSupabaseMock(); mockRuntime(client, undefined, { localNoAuth: true }); - const { GET } = await import("../src/app/api/documents/route"); + const { POST } = await import("../src/app/api/upload/route"); + const formData = new FormData(); + formData.set("file", new File(["hello"], "guideline.txt", { type: "text/plain" })); - const response = await GET(localPortRequest(3000, "/api/documents")); + const response = await POST( + localPortRequest(3000, "/api/upload", { + method: "POST", + body: formData, + }), + ); expect(response.status).toBe(401); expect(client.auth.getUser).not.toHaveBeenCalled(); expect(client.from).not.toHaveBeenCalled(); }); - it("rejects managed-port private calls with stale localhost referers before Supabase access", async () => { + it("rejects managed-port upload calls with stale localhost referers before Supabase access", async () => { const client = createSupabaseMock(); mockRuntime(client, undefined, { localNoAuth: true }); - const { GET } = await import("../src/app/api/documents/route"); + const { POST } = await import("../src/app/api/upload/route"); + const formData = new FormData(); + formData.set("file", new File(["hello"], "guideline.txt", { type: "text/plain" })); - const response = await GET( - localPortRequest(4298, "/api/documents", { + const response = await POST( + localPortRequest(4298, "/api/upload", { + method: "POST", headers: { referer: "http://localhost:3000/", }, + body: formData, }), ); @@ -428,66 +443,20 @@ describe("private document API access", () => { expect(client.from).not.toHaveBeenCalled(); }); - it("resolves local no-auth owner from documents before listing auth users", async () => { - const documents = [{ id: documentId, owner_id: userId, title: "Owned document" }]; - const client = createSupabaseMock((call) => { - if (call.table === "documents" && call.selected === "owner_id") { - return ok({ owner_id: userId }); - } - if (call.table === "documents") { - return ok(documents); - } - return ok([]); - }); - mockRuntime(client, undefined, { localNoAuth: true }); + it("lists public documents for unauthenticated callers", async () => { + const documents = [{ id: documentId, owner_id: null, title: "Public guideline", status: "indexed" }]; + const client = createSupabaseMock((call) => (call.table === "documents" ? ok(documents) : ok([]))); + mockRuntime(client); const { GET } = await import("../src/app/api/documents/route"); - const response = await GET(localPortRequest(4298, "/api/documents")); + const response = await GET(request("/api/documents?includeMeta=false")); const body = await payload(response); - expect(response.status).toBe(401); - expect(body).toEqual({ error: "Authentication required." }); + expect(response.status).toBe(200); + expect(body.documents).toEqual(documents); expect(client.auth.getUser).not.toHaveBeenCalled(); - expect(client.from).not.toHaveBeenCalled(); - }); - - it("resolves configured local no-auth owner email before document fallback", async () => { - const documents = [{ id: documentId, owner_id: userId, title: "Configured owner document" }]; - const client = createSupabaseMock((call) => { - if (call.table === "documents" && call.selected === "owner_id") { - return ok({ owner_id: otherUserId }); - } - if (call.table === "documents") { - return ok(documents); - } - return ok([]); - }); - client.auth.admin.listUsers.mockResolvedValueOnce({ - data: { users: [{ id: userId, email: "clinician@example.test" }], nextPage: 0 }, - error: null, - }); - mockRuntime(client, undefined, { localNoAuth: true, localOwnerEmail: "clinician@example.test" }); - const { GET } = await import("../src/app/api/documents/route"); - - const response = await GET(localPortRequest(4298, "/api/documents")); - const body = await payload(response); - - expect(response.status).toBe(401); - expect(body).toEqual({ error: "Authentication required." }); - expect(client.auth.admin.listUsers).not.toHaveBeenCalled(); - expect(client.from).not.toHaveBeenCalled(); - }); - - it("rejects unauthenticated document listing", async () => { - const client = createSupabaseMock(); - mockRuntime(client); - const { GET } = await import("../src/app/api/documents/route"); - - const response = await GET(request("/api/documents")); - - expect(response.status).toBe(401); - expect(await payload(response)).toEqual({ error: "Authentication required." }); - expect(client.from).not.toHaveBeenCalled(); + expect(client.calls[0].filters).toContainEqual({ column: "owner_id", value: null }); + expect(client.calls[0].filters).not.toContainEqual({ column: "owner_id", value: userId }); }); it("filters authenticated document listing by owner", async () => { @@ -602,6 +571,48 @@ describe("private document API access", () => { expect(client.storageMocks.createSignedUrl).not.toHaveBeenCalled(); }); + it("allows anonymous signed URLs for public documents", async () => { + const client = createSupabaseMock((call) => { + if ( + call.table === "documents" && + call.filters.some((filter) => filter.column === "owner_id" && filter.value === null) + ) { + return ok({ storage_path: "public/documents/guideline.pdf", file_type: "application/pdf" }); + } + return ok(null); + }); + mockRuntime(client); + const { GET } = await import("../src/app/api/documents/[id]/signed-url/route"); + + const response = await GET(request(`/api/documents/${documentId}/signed-url`), { + params: Promise.resolve({ id: documentId }), + }); + const body = await payload(response); + + expect(response.status).toBe(200); + expect(body.url).toContain("public/documents/guideline.pdf"); + expect(client.auth.getUser).not.toHaveBeenCalled(); + }); + + it("rejects anonymous signed URLs for private documents", async () => { + const client = createSupabaseMock((call) => { + if (call.table === "documents" && call.filters.some((filter) => filter.column === "owner_id")) { + return ok(null); + } + return ok(null); + }); + mockRuntime(client); + const { GET } = await import("../src/app/api/documents/[id]/signed-url/route"); + + const response = await GET(request(`/api/documents/${otherDocumentId}/signed-url`), { + params: Promise.resolve({ id: otherDocumentId }), + }); + + expect(response.status).toBe(404); + expect(await payload(response)).toEqual({ error: "Document not found." }); + expect(client.storageMocks.createSignedUrl).not.toHaveBeenCalled(); + }); + it("allows image signed URLs only when the parent document is owned", async () => { const client = createSupabaseMock((call) => { if (call.table === "document_images") { @@ -2788,6 +2799,41 @@ describe("private document API access", () => { ); }); + it("degrades invalid bearer tokens to anonymous search scope", async () => { + const searchChunksWithTelemetry = vi.fn(async () => ({ + results: [], + telemetry: { + search_cache_hit: false, + text_fast_path_latency_ms: 0, + embedding_skipped: true, + embedding_latency_ms: 0, + embedding_cache_hit: false, + supabase_rpc_latency_ms: 0, + rerank_latency_ms: 0, + retrieval_strategy: "text_fast_path", + }, + })); + const client = createSupabaseMock(); + mockRuntime(client, { searchChunksWithTelemetry }); + const searchRoute = await import("../src/app/api/search/route"); + + const response = await searchRoute.POST( + request("/api/search", { + method: "POST", + headers: { + authorization: "Bearer expired-token", + "content-type": "application/json", + }, + body: JSON.stringify({ query: "monitoring" }), + }), + ); + + expect(response.status).toBe(200); + expect(searchChunksWithTelemetry).toHaveBeenCalledWith( + expect.objectContaining({ ownerId: undefined, allowGlobalSearch: true }), + ); + }); + it("rate limits anonymous answer bursts before generation", async () => { const answerQuestionWithScope = vi.fn(async () => ({ answer: "Public evidence.", diff --git a/tests/registry-records-route.test.ts b/tests/registry-records-route.test.ts index 388bd52e5..5571c270c 100644 --- a/tests/registry-records-route.test.ts +++ b/tests/registry-records-route.test.ts @@ -198,11 +198,8 @@ describe("registry records API", () => { expect(payload.records.some((record) => record.slug === "13yarn")).toBe(true); expect(payload.matches?.[0]?.record.slug).toBe("13yarn"); expect(client.from).not.toHaveBeenCalled(); - expect(client.rpc).toHaveBeenCalledWith( - "consume_api_subject_rate_limit", - expect.objectContaining({ p_bucket: "registry" }), - ); - expect(client.rpc).not.toHaveBeenCalledWith("consume_api_rate_limit", expect.anything()); + expect(client.rpc).not.toHaveBeenCalled(); + expect(client.auth.getUser).not.toHaveBeenCalled(); }); it("rejects an invalid kind with a validation error", async () => { @@ -311,11 +308,8 @@ describe("registry records API", () => { expect(payload.record.slug).toBe("13yarn"); expect(payload.linkedDocuments).toEqual([]); expect(client.from).not.toHaveBeenCalled(); - expect(client.rpc).toHaveBeenCalledWith( - "consume_api_subject_rate_limit", - expect.objectContaining({ p_bucket: "registry" }), - ); - expect(client.rpc).not.toHaveBeenCalledWith("consume_api_rate_limit", expect.anything()); + expect(client.rpc).not.toHaveBeenCalled(); + expect(client.auth.getUser).not.toHaveBeenCalled(); }); it("returns 404 for an unknown slug", async () => {