From 95ab65dbdf39b27a860a662306b931eedfdb0e03 Mon Sep 17 00:00:00 2001 From: Georgy Butaev <41178744+g-but@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:32:14 +0200 Subject: [PATCH] =?UTF-8?q?fix(invitations):=20a=20token=20could=20never?= =?UTF-8?q?=20be=20generated=20=E2=80=94=20pgcrypto=20is=20in=20another=20?= =?UTF-8?q?schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit generate_invitation_token() calls gen_random_bytes(24) unqualified and declares no search_path, so the name resolves against whatever the caller has. pgcrypto lives in `extensions`, the normal Supabase layout, and PostgREST calls with `public` on the path — so the function raised "function gen_random_bytes(integer) does not exist" every time. No invitation token has ever been generated. Same family as the timeline functions in 20260828110000: a reference resolved only at call time, so it reads as correct until somebody uses it. Found by the plpgsql_check gate added alongside them — the first thing able to see this class at all, and it found this one within minutes of shipping. The call is now schema-qualified, and search_path is pinned, which this function did not set at all. An unpinned search_path lets a caller shadow a name the function resolves, which is worth closing on a function whose job is minting a credential. Rehearsed against production and rolled back: raises before, returns a 32-character token after, and the broken-function count falls 12 → 11, matching the baseline the nightly gate now holds. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012dpTLxh5GJWeWTF1UEvcD5 --- ..._invitation_token_cannot_find_pgcrypto.sql | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 supabase/migrations/20260828140000_invitation_token_cannot_find_pgcrypto.sql diff --git a/supabase/migrations/20260828140000_invitation_token_cannot_find_pgcrypto.sql b/supabase/migrations/20260828140000_invitation_token_cannot_find_pgcrypto.sql new file mode 100644 index 000000000..4dec3e6e8 --- /dev/null +++ b/supabase/migrations/20260828140000_invitation_token_cannot_find_pgcrypto.sql @@ -0,0 +1,36 @@ +-- Generating an invitation token raises, because it looks for pgcrypto in the +-- wrong schema. +-- +-- `generate_invitation_token()` calls `gen_random_bytes(24)` unqualified and +-- declares no search_path, so the name resolves against whatever the CALLER +-- has. pgcrypto is installed in `extensions`, not `public` — which is the +-- normal Supabase layout — and PostgREST calls with `public` on the path. So +-- the function raises `function gen_random_bytes(integer) does not exist` every +-- time, and no invitation token has ever been generated. +-- +-- Same family as the timeline functions repaired in 20260828110000: a +-- reference that is only resolved at call time, so it looks correct until +-- somebody uses it. Found by the plpgsql_check gate added alongside them, which +-- is the first thing that has ever been able to see this class. +-- +-- Two changes, and the first is the fix: +-- +-- * qualify the call as `extensions.gen_random_bytes`, so it resolves by +-- name rather than by whoever happens to be calling; +-- * pin `search_path`, which this function did not set at all. An unpinned +-- search_path on a function is also how a caller can shadow a name it +-- resolves — worth closing on a function whose whole job is minting a +-- credential. +-- +-- Deliberately NOT moving pgcrypto into public: extensions belong in their own +-- schema, and every other caller already reaches them there. + +CREATE OR REPLACE FUNCTION public.generate_invitation_token() +RETURNS text +LANGUAGE plpgsql +SET search_path TO 'public', 'extensions' +AS $function$ +BEGIN + RETURN encode(extensions.gen_random_bytes(24), 'base64'); +END; +$function$;