Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions supabase/drift-manifest.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"generated_at": "2026-07-18T08:58:41.495Z",
"generated_at": "2026-07-18T14:23:15.039Z",
"generator": "scripts/generate-drift-manifest.ts",
"postgres_image": "supabase/postgres:17.6.1.127",
"schema_sha256": "302a9ecec8e69564d50035be8480fa5e5686ca6136d96978f2188ecf5fb58be1",
"replay_seconds": 18,
"schema_sha256": "3a2bd3f0de8dd1d16857100556c9bee604b063eedcb9eb5e7ab959e4799d28f0",
"replay_seconds": 20,
"snapshot": {
"views": [
{
Expand Down Expand Up @@ -6335,6 +6335,11 @@
"name": "document_sections_updated_at",
"table": "document_sections"
},
{
"def": "CREATE TRIGGER document_title_words_enforce_public_scope BEFORE INSERT OR UPDATE ON public.document_title_words FOR EACH ROW EXECUTE FUNCTION public.enforce_document_title_word_scope()",
"name": "document_title_words_enforce_public_scope",
"table": "document_title_words"
},
{
"def": "CREATE TRIGGER documents_require_publication_approval BEFORE INSERT OR UPDATE ON public.documents FOR EACH ROW EXECUTE FUNCTION public.guard_document_publication_transition()",
"name": "documents_require_publication_approval",
Expand Down Expand Up @@ -6597,6 +6602,13 @@
"def_hash": "4f3e6b143eba09bf1bed09b6ec9f71b8",
"signature": "public.document_summary_text(uuid)"
},
{
"acl": [
"postgres=X/postgres"
],
"def_hash": "2314085d7e21c5d54f3997e00fd8cdd9",
"signature": "public.enforce_document_title_word_scope()"
},
{
"acl": [
"postgres=X/postgres",
Expand Down
14 changes: 14 additions & 0 deletions supabase/migrations/20260717171000_public_title_corrector.sql
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,20 @@ create trigger documents_sync_title_words
after insert or update of title, status, owner_id or delete on public.documents
for each row execute function public.sync_document_title_words();

-- Purge vocabulary created by 20260714180000 before installing the
-- table-backed corrector below. A later forward migration repeats this cleanup
-- for environments where this migration was already applied.
delete from public.document_title_words dtw
where not exists (
select 1
from public.documents d
where d.id = dtw.document_id
and d.owner_id is null
and d.status = 'indexed'
and length(dtw.word) between 4 and 40
and dtw.word = any (regexp_split_to_array(lower(d.title), '[^a-z]+'))
);

insert into public.document_title_words (word, document_id)
select distinct lower(title_word), d.id
from public.documents d
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
-- Remove historical private/non-indexed title vocabulary and make the public
-- scope an enforced database invariant.
--
-- 20260714180000 populated document_title_words from every indexed document.
-- 20260717171000 made future document-trigger writes public-only, but its
-- ON CONFLICT backfill did not remove the already-present private rows. The
-- SECURITY DEFINER corrector reads this table directly, so those rows remained
-- observable through query correction.
Comment thread
BigSimmo marked this conversation as resolved.

set lock_timeout = '5s';
set statement_timeout = '60s';

alter table public.document_title_words enable row level security;
revoke all on table public.document_title_words from public, anon, authenticated;
grant select, insert, update, delete on table public.document_title_words to service_role;

do $$
begin
if not exists (
select 1
from pg_catalog.pg_constraint
where conrelid = 'public.document_title_words'::pg_catalog.regclass
and conname = 'document_title_words_word_length'
) then
alter table public.document_title_words
add constraint document_title_words_word_length
check (pg_catalog.length(word) between 4 and 40) not valid;
end if;

if not exists (
select 1
from pg_catalog.pg_constraint
where conrelid = 'public.document_title_words'::pg_catalog.regclass
and conname = 'document_title_words_lowercase'
) then
alter table public.document_title_words
add constraint document_title_words_lowercase
check (word = pg_catalog.lower(word)) not valid;
end if;
end;
$$;

create or replace function public.enforce_document_title_word_scope()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
begin
perform 1
from public.documents d
where d.id = new.document_id
and d.owner_id is null
and d.status = 'indexed'
and pg_catalog.length(new.word) between 4 and 40
and new.word = any (
pg_catalog.regexp_split_to_array(pg_catalog.lower(d.title), '[^a-z]+')
)
for share;

if not found then
raise exception 'document_title_words rows require a current indexed public document title'
using errcode = '23514';
end if;

return new;
end;
$$;

revoke execute on function public.enforce_document_title_word_scope()
from public, anon, authenticated, service_role;

drop trigger if exists document_title_words_enforce_public_scope
on public.document_title_words;
create trigger document_title_words_enforce_public_scope
before insert or update on public.document_title_words
for each row execute function public.enforce_document_title_word_scope();

-- Purge every legacy row that is no longer an exact word from a current,
-- indexed, null-owner document title. This removes private/non-indexed rows as
-- well as any stale word left by historical trigger behavior.
delete from public.document_title_words dtw
where not exists (
select 1
from public.documents d
where d.id = dtw.document_id
and d.owner_id is null
and d.status = 'indexed'
and pg_catalog.length(dtw.word) between 4 and 40
and dtw.word = any (
pg_catalog.regexp_split_to_array(pg_catalog.lower(d.title), '[^a-z]+')
)
);

-- Repair any missing public vocabulary rows after cleanup. The scope trigger
-- above validates each inserted row before it can become visible.
insert into public.document_title_words (word, document_id)
select distinct pg_catalog.lower(title_word), d.id
from public.documents d
cross join lateral pg_catalog.unnest(
pg_catalog.regexp_split_to_array(pg_catalog.lower(d.title), '[^a-z]+')
) as title_word
where d.owner_id is null
and d.status = 'indexed'
and pg_catalog.length(title_word) between 4 and 40
on conflict do nothing;

alter table public.document_title_words
validate constraint document_title_words_word_length;
alter table public.document_title_words
validate constraint document_title_words_lowercase;

do $$
begin
if exists (
select 1
from public.document_title_words dtw
where not exists (
select 1
from public.documents d
where d.id = dtw.document_id
and d.owner_id is null
and d.status = 'indexed'
and pg_catalog.length(dtw.word) between 4 and 40
and dtw.word = any (
pg_catalog.regexp_split_to_array(pg_catalog.lower(d.title), '[^a-z]+')
)
)
) then
raise exception 'document_title_words contains rows outside the indexed public title corpus'
using errcode = '23514';
end if;
end;
$$;

-- Preserve the trigger-function and corrector privilege posture established by
-- the earlier hardening migrations.
revoke execute on function public.sync_document_title_words()
from public, anon, authenticated, service_role;
revoke execute on function public.correct_clinical_query_terms(text, real)
from public, anon, authenticated;
grant execute on function public.correct_clinical_query_terms(text, real)
to service_role;

-- 20260717173000 establishes this as the fail-closed default-ACL invariant.
-- This newer migration creates a function, so reassert that the invariant still
-- holds before the transaction can commit.
do $$
declare
v_status jsonb;
begin
if not exists (select 1 from pg_catalog.pg_roles where rolname = 'supabase_admin') then
raise notice 'role supabase_admin does not exist; default-privilege assertion is not applicable';
return;
end if;

v_status := public.default_privileges_status('supabase_admin', 'public');
if not coalesce((v_status->>'safe')::boolean, false) then
raise exception using
errcode = '42501',
message = 'Unsafe supabase_admin default privileges; title-word privacy migration blocked.',
detail = v_status::text,
hint = 'Reapply the default-privilege remediation in migration 20260717173000, then retry.';
end if;
end;
$$;
57 changes: 54 additions & 3 deletions supabase/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5094,17 +5094,67 @@ $$;

revoke execute on function public.sync_document_title_words()
from public, anon, authenticated, service_role;

create or replace function public.enforce_document_title_word_scope()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
begin
perform 1
from public.documents d
where d.id = new.document_id
and d.owner_id is null
and d.status = 'indexed'
and pg_catalog.length(new.word) between 4 and 40
and new.word = any (
pg_catalog.regexp_split_to_array(pg_catalog.lower(d.title), '[^a-z]+')
)
for share;

if not found then
raise exception 'document_title_words rows require a current indexed public document title'
using errcode = '23514';
end if;
return new;
end;
$$;

revoke execute on function public.enforce_document_title_word_scope()
from public, anon, authenticated, service_role;
drop trigger if exists document_title_words_enforce_public_scope
on public.document_title_words;
create trigger document_title_words_enforce_public_scope
before insert or update on public.document_title_words
for each row execute function public.enforce_document_title_word_scope();

drop trigger if exists documents_sync_title_words on public.documents;
create trigger documents_sync_title_words
after insert or update of title, status, owner_id or delete on public.documents
for each row execute function public.sync_document_title_words();

delete from public.document_title_words dtw
where not exists (
select 1
from public.documents d
where d.id = dtw.document_id
and d.owner_id is null
and d.status = 'indexed'
and pg_catalog.length(dtw.word) between 4 and 40
and dtw.word = any (
pg_catalog.regexp_split_to_array(pg_catalog.lower(d.title), '[^a-z]+')
)
);

insert into public.document_title_words (word, document_id)
select distinct lower(title_word), d.id
select distinct pg_catalog.lower(title_word), d.id
from public.documents d
cross join lateral unnest(regexp_split_to_array(lower(d.title), '[^a-z]+')) as title_word
cross join lateral pg_catalog.unnest(
pg_catalog.regexp_split_to_array(pg_catalog.lower(d.title), '[^a-z]+')
) as title_word
where d.owner_id is null and d.status = 'indexed'
and length(title_word) between 4 and 40
and pg_catalog.length(title_word) between 4 and 40
on conflict do nothing;

create or replace function public.correct_clinical_query_terms(
Expand Down Expand Up @@ -5238,6 +5288,7 @@ grant usage, select on all sequences in schema public to service_role;
grant execute on all functions in schema public to service_role;
revoke execute on function public.cleanup_registry_corpus_document() from service_role;
revoke execute on function public.sync_document_title_words() from service_role;
revoke execute on function public.enforce_document_title_word_scope() from service_role;
revoke execute on function public.claim_indexing_v3_agent_jobs(text, integer, integer) from public, anon, authenticated;
grant execute on function public.claim_indexing_v3_agent_jobs(text, integer, integer) to service_role;
revoke execute on function public.match_document_embedding_fields_text(text, integer, double precision, uuid[], uuid) from public, anon, authenticated;
Expand Down
Loading