Skip to content

privacy: enforce query-hash secret in prod + purge rag_query_misses (PIA-2, PIA-4)#368

Merged
BigSimmo merged 3 commits into
mainfrom
claude/privacy-tenancy-hardening
Jul 8, 2026
Merged

privacy: enforce query-hash secret in prod + purge rag_query_misses (PIA-2, PIA-4)#368
BigSimmo merged 3 commits into
mainfrom
claude/privacy-tenancy-hardening

Conversation

@BigSimmo

@BigSimmo BigSimmo commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Implements the two low-risk, locally-verifiable fixes from the privacy impact assessment. Follow-up to the analysis in #330.

PIA-2 (High) — require RAG_QUERY_HASH_SECRET in production

Clinical query text is redacted to a keyed HMAC pseudonym before logging (query-privacy.ts). Without the secret, the hash silently degrades to unsalted, dictionary-reversible SHA-256 — a reader of the log tables can hash candidate patient/drug strings offline and match rows. Nothing enforced the secret.

  • New requireQueryHashSecret() in src/lib/env.ts, called from instrumentation.register() — the existing production-only startup gate that already fails closed on demo/no-auth/misconfig. Missing secret now refuses to start with an actionable error.
  • Unit test: tests/env-query-hash-secret.test.ts.

PIA-4 (Medium) — bound rag_query_misses retention

It stored the same hash-redacted query telemetry as rag_queries (30d) and rag_retrieval_logs (90d) but had no purge, so rows accumulated indefinitely.

  • New migration 20260708120000_rag_query_misses_retention.sql: purge_expired_rag_query_misses(90) + nightly pg_cron job purge-rag-query-misses @ 03:45 UTC (installs the function even where pg_cron is absent, matching the existing retention migrations).
  • schema.sql + drift-manifest.json are intentionally not hand-edited — they're regenerated from live via npm run drift:manifest (Docker) as the standard follow-up after the migration applies.

Deliberately not in this PR

The fail-closed retrieval_owner_matches change (tenancy review §6, item 1). It's on the retrieval hot path with ~20 RPC call sites and internal default-null callers, and its only real verification is the live golden eval, which can't run without the Supabase key. Its production benefit is a future-regression backstop (prod already throws before null reaches the RPC). It'll come as its own verified change rather than shipped blind.

Verification

typecheck, eslint, prettier clean; vitest green for env-query-hash-secret, env-demo-mode, privacy, owner-scope, supabase-schema, drift-detection, check-runtime, api-route-coverage. Live pg_cron scheduling is only verifiable against the live project.

Operator action

Set RAG_QUERY_HASH_SECRET (random, ≥16 chars) on the live project before this deploys, or production startup will fail closed (by design).

🤖 Generated with Claude Code

Two low-risk hardening fixes from the privacy impact assessment (PIA-2, PIA-4).

PIA-2 — RAG_QUERY_HASH_SECRET is now required at production startup. Without it,
logged clinical-query hashes silently degrade from keyed HMAC-SHA256 to unsalted,
dictionary-reversible SHA-256, defeating the redaction. requireQueryHashSecret()
(src/lib/env.ts) is called from instrumentation.register(), which already fails
closed on demo/no-auth/misconfig; unit-tested in tests/env-query-hash-secret.test.ts.

PIA-4 — add a nightly pg_cron purge for rag_query_misses (90 days), which stored
the same hash-redacted query telemetry as rag_queries/rag_retrieval_logs but had
no retention bound. New migration only; schema.sql + drift-manifest are regenerated
from live via `npm run drift:manifest` (Docker) as the usual follow-up.

Not included: the fail-closed retrieval_owner_matches change (tenancy review §6
item 1) — it sits on the retrieval hot path with ~20 RPC sites and needs the live
golden-eval to verify, which can't run without the Supabase key. Held as a
separate, verified change.

Verified: typecheck, eslint, prettier, and the relevant vitest suites (env,
privacy, owner-scope, supabase-schema, drift-detection, check-runtime,
api-route-coverage) pass. Live cron scheduling is verifiable only against the live
project.

Co-Authored-By: Claude Fable 5 <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

Updates to Preview Branch (claude/privacy-tenancy-hardening) ↗︎

Deployments Status Updated
Database Wed, 08 Jul 2026 06:26:38 UTC
Services Wed, 08 Jul 2026 06:26:38 UTC
APIs Wed, 08 Jul 2026 06:26:38 UTC

Tasks are run on every commit but only new migration files are pushed.
Close and reopen this PR if you want to apply changes from existing seed or migration files.

Tasks Status Updated
Configurations Wed, 08 Jul 2026 06:26:40 UTC
Migrations Wed, 08 Jul 2026 06:26:42 UTC
Seeding Wed, 08 Jul 2026 06:26:43 UTC
Edge Functions Wed, 08 Jul 2026 06:26:44 UTC

View logs for this Workflow Run ↗︎.
Learn more about Supabase for Git ↗︎.

The startup guard added in this branch (requireQueryHashSecret) makes the
secret mandatory in production, so update .env.example from "strongly
recommended" to "required", with a generation command. Placeholder only — no
real value committed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@BigSimmo
BigSimmo enabled auto-merge (squash) July 8, 2026 06:21
The new requireQueryHashSecret() gate made the existing "starts a fully
configured production server" case throw. Supply RAG_QUERY_HASH_SECRET in that
case and add a case asserting register() rejects when it is absent (ordered
after the OpenAI check, matching instrumentation.ts). Full vitest suite green
(1292 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@BigSimmo
BigSimmo merged commit c98c47d into main Jul 8, 2026
5 checks passed
@BigSimmo
BigSimmo deleted the claude/privacy-tenancy-hardening branch July 9, 2026 12:35
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