Skip to content

fix(search): stop /api/search/universal serving demo docs to live public users#373

Merged
BigSimmo merged 2 commits into
mainfrom
claude/beautiful-hugle-dd3fb8
Jul 8, 2026
Merged

fix(search): stop /api/search/universal serving demo docs to live public users#373
BigSimmo merged 2 commits into
mainfrom
claude/beautiful-hugle-dd3fb8

Conversation

@BigSimmo

@BigSimmo BigSimmo commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Summary

/api/search/universal (the cross-entity typeahead) short-circuited anonymous / no-owner callers to runUniversalSearch({ demo: true }). For the documents domain that serves the synthetic demo corpus (demoSearch) instead of the real indexed public corpus. Because isDemoMode() is false in production, every unauthenticated visitor using the typeahead was shown demo documents, while /api/search and /api/answer already served them the real public corpus.

This drops both demo short-circuits so non-demo callers always run the live pipeline (demo:false + admin client):

  • Anonymous callers resolve to ownerId: undefinedallowGlobalSearch, scoping the documents domain to the public corpus (owner_id IS NULL) — the same pattern /api/search uses. Registry domains (medications/services/forms/differentials/tools) already served the real default catalogues, so those are unchanged.
  • The anonymous path is now rate limited (it previously bypassed the limiter entirely).
  • Only an explicit demo/local deploy (isDemoMode() / isLocalNoAuthMode()) still serves fixtures.

Anonymous responses keep the publicAccess: true flag; owner responses are unchanged.

Regression tests added in tests/universal-search.test.ts: an anonymous live request runs demo:false against the public corpus (not fixtures), the anonymous path returns 429 when throttled, and the authenticated owner path is unchanged.

Verification

  • npx tsc --noEmit (typecheck) — clean
  • npx eslint + npx prettier --check on changed files — clean
  • npx vitest run on universal-search + access-contract suites (api-route-coverage, public-access-deep, private-access-routes, registry-records-route) — 140/140 pass
  • npm run verify:cheap / check:production-readiness — not run here: this worktree has a partial node_modules and no live Supabase keys ([demo mode]). Recommend CI + a maintainer with live keys run these.
  • eval:retrieval:qualitynot applicable: no retrieval/ranking/selection/chunking/scoring logic changed. This is an access-routing fix; the anonymous path now uses the same live pipeline /api/search already runs.

Clinical Governance Preflight

  • Source-backed claims still require linked source verification before clinical use — unchanged
  • No patient-identifiable document workflow was introduced or expanded
  • Supabase target remains Clinical KB Database (sjrfecxgysukkwxsowpy) — untouched
  • Service-role keys and private document access remain server-only — unchanged (still admin-client + owner-scoped)
  • Demo/synthetic content remains clearly separated from real clinical sources — this PR enforces that separation: demo docs are no longer served to live public users
  • Source metadata, review status, and outdated/unknown-source behavior remain conservative — unchanged
  • No clinical decision-support behavior change (TGA SaMD impact n/a): this changes which corpus anonymous callers search, not answer generation

Notes

  • Anonymous callers now hit the live retrieval pipeline (Supabase + embeddings) on the typeahead. The newly-enforced registry-bucket rate limit bounds cost/abuse on that path.
  • Follow-up (pre-existing, not in this PR): reconcile the Supabase Preview migration drift noted on main.

🤖 Generated with Claude Code

…lic users

The universal typeahead route short-circuited anonymous (no-cookie) and
owner-less callers to runUniversalSearch({ demo: true }), which serves the
synthetic demo corpus (demoSearch) for the documents domain instead of the
real indexed public corpus. In production isDemoMode() is false, so every
unauthenticated visitor hitting the typeahead was shown demo documents while
/api/search and /api/answer served them the real public corpus.

Drop both demo short-circuits so non-demo callers always run the live
pipeline (demo:false + admin client). Anonymous callers resolve to ownerId
undefined -> allowGlobalSearch, scoping the documents domain to the public
corpus (owner_id IS NULL) exactly like /api/search; registry domains already
used the real default catalogues. The anonymous path is now rate limited (it
previously bypassed the limiter). Only an explicit demo/local deploy still
serves fixtures.

Adds regression tests: an anonymous live request runs demo:false against the
public corpus, the anonymous path is rate limited, and the owner path is
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@supabase

supabase Bot commented Jul 8, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project sjrfecxgysukkwxsowpy because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@BigSimmo
BigSimmo enabled auto-merge July 8, 2026 06:28
@BigSimmo
BigSimmo merged commit 39a436d into main Jul 8, 2026
5 checks passed
@BigSimmo

BigSimmo commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

Follow-up: structural guard so this class can't recur

Added a second commit that closes the whole demo-served-live class, not just this route.

demoSearch() — the single choke point for synthetic document search (demoAnswer routes through it too) — now fails closed: it throws in a production request unless NEXT_PUBLIC_DEMO_MODE=true. In production the Supabase-config demo fallback and isLocalNoAuthMode are already disabled, so an explicit demo deploy is the only legitimate case. The guard reads process.env directly (not isDemoMode) so it can't be silently disabled by a test that mocks @/lib/env, and so every current and future caller is covered — a new route can't reintroduce the hard-coded demo:true mistake without a loud 500.

Mirrors the existing fail-closed requireOwnerScope retrieval guard. +2 regression tests. Verified: tsc clean; 169 tests green across the demo/answer/access suites (incl. the non-production Supabase-failure fallback paths, which still work).

Safety note also verified for reviewers: the anonymous path uses allowGlobalSearchretrievalOwnerFilter returns PUBLIC_OWNER_FILTER_SENTINEL (00000000-… = owner_id IS NULL), i.e. the public corpus only — it does not expose other tenants' private documents, and real retrieval without an owner/global scope throws.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant