diff --git a/crates/manager-core/migrations/0039_app_user_invitations_unique_pending.sql b/crates/manager-core/migrations/0039_app_user_invitations_unique_pending.sql new file mode 100644 index 0000000..61658e8 --- /dev/null +++ b/crates/manager-core/migrations/0039_app_user_invitations_unique_pending.sql @@ -0,0 +1,13 @@ +-- F-S-013 (audit 2026-06-07): partial unique index on pending +-- (app_id, lower(email)) for app_user_invitations. +-- +-- Without it, users::invite("alice@…") called N times creates N rows +-- with N valid tokens. Combined with accept_invite returning Ok(None) +-- silently when the email already exists, an attacker who learns one +-- invitation token can permanently consume it without effect. +-- +-- The constraint is partial so re-inviting after a previous invitation +-- was accepted still works (the accepted row sits outside the index). +CREATE UNIQUE INDEX IF NOT EXISTS idx_app_user_invitations_unique_pending + ON app_user_invitations (app_id, lower(email)) + WHERE accepted_at IS NULL;