Found while reviewing PR #11767 (audience posture, #11739). Fail-closed, so not a security hole — a functional ceiling in the invitation carve-out.
AuthManager.hasPendingInvitationFor(email) (plugin-auth auth-manager.ts) resolves "does this address hold a pending invitation?" by reading at most 200 pending rows and scanning them in memory for a case-insensitive email match:
const raw = await reader.find('sys_invitation', { where: { status: 'pending' }, limit: 200 });
Above 200 concurrently-pending invitations in one environment, an invitee outside the first page is not found. Under the new default posture invite_only that means: an administrator sends an invitation, the invitee tries to create their account, and registration is refused with SELF_REGISTRATION_CLOSED — the invitation lane silently stops working for the tail of a large rollout, with no signal to either party. A 500-employee onboarding is an ordinary way to reach that ceiling, and it is the exact flow the audience-posture epic exists to make work.
The in-memory scan appears to exist so the match can be case-insensitive. Likely fixes (implementer judges): filter on the email in the query (better-auth lowercases invitation emails on write — verify) and drop the scan; or keep the scan but page until exhausted; or add a driver-supported case-insensitive predicate. Whatever lands should be pinned by a test with more than one page of pending rows — the current suite covers expiry and case-insensitivity but not the page boundary, which is why the ceiling is invisible.
Not fixed in #11767: the carve-out is the dev's own scoped addition there and the behaviour is fail-closed, so this was filed rather than sent back for a rework round.
Found while reviewing PR #11767 (audience posture, #11739). Fail-closed, so not a security hole — a functional ceiling in the invitation carve-out.
AuthManager.hasPendingInvitationFor(email)(plugin-authauth-manager.ts) resolves "does this address hold a pending invitation?" by reading at most 200 pending rows and scanning them in memory for a case-insensitive email match:Above 200 concurrently-pending invitations in one environment, an invitee outside the first page is not found. Under the new default posture
invite_onlythat means: an administrator sends an invitation, the invitee tries to create their account, and registration is refused withSELF_REGISTRATION_CLOSED— the invitation lane silently stops working for the tail of a large rollout, with no signal to either party. A 500-employee onboarding is an ordinary way to reach that ceiling, and it is the exact flow the audience-posture epic exists to make work.The in-memory scan appears to exist so the match can be case-insensitive. Likely fixes (implementer judges): filter on the email in the query (better-auth lowercases invitation emails on write — verify) and drop the scan; or keep the scan but page until exhausted; or add a driver-supported case-insensitive predicate. Whatever lands should be pinned by a test with more than one page of pending rows — the current suite covers expiry and case-insensitivity but not the page boundary, which is why the ceiling is invisible.
Not fixed in #11767: the carve-out is the dev's own scoped addition there and the behaviour is fail-closed, so this was filed rather than sent back for a rework round.