2 Commits
Author SHA1 Message Date
Moheeb ZaraandClaude Opus 4.8 61ffc2e01f fix(invites): distinguish CAS contention from a real use-cap (retry vs reject)
claimInviteUse returned null both when the cap was truly reached and when it
lost the compare-and-swap 5× under load, so a legitimate joiner on a busy
high-cap invite got a false 'invite has reached its use cap'. Return a
discriminated result; map contention to a distinct 'try again' message.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-04 19:10:52 -07:00
Moheeb ZaraandClaude Opus 4.8 2ee929658e fix(invites): close single-use invite max_uses TOCTOU (H1)
Redeeming an invite read uses_count, checked the cap in JS, then did a
non-atomic uses_count = uses_count + 1 write AFTER creating the membership.
Two concurrent redemptions of a max_uses=1 invite both passed the check and
both created a member, and the counter under-counted.

Fix: claim a use ATOMICALLY before creating the membership via a
compare-and-swap on uses_count (lib/invite-claim.ts) — the UPDATE only writes
when uses_count still equals the value the cap decision was based on, so of two
racing claims exactly one wins. The loser re-reads and retries while capacity
remains (multi-use invites unaffected). On a failed membership insert the claim
is rolled back so a slot is not burned.

Extracted to lib/invite-claim.ts (out of the 'use server' surface, unit-tested).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 21:16:12 -07:00