@@ -135,15 +135,31 @@ and hard to cap/expire/audit ("when was this credited?"). Good for simple boosts
135135> Recommended: **A + reconcile cron** for credit/cashback economies; **B** for static
136136> entitlement boosts. The substrate supports both simultaneously.
137137
138- ## Gates before shipping rewards (#3833 — read it first)
139-
140- 1. **Scope the list**: ` GET / api/ invitations` is platform-global today (admin-only by
141- CASL). Before widening ` create Invitation` to regular users, add an ` invitedBy` -scoped
142- ` / mine` (PII: invitee emails).
143- 2. **Self-referral guard** — alternate-email self-invites become valuable once credits exist.
144- 3. **Open-signup hole** — claim/finalize are gated on ` ! config .sign .up ` : with public signup
145- open the event never fires. Resolve (finalize-without-claim) or hide the Referrals tab
146- before any open deployment enables rewards.
138+ ## Gates before shipping rewards (#3833 — status)
139+
140+ 1. **Scope the list — SHIPPED (#3833)**: ` GET / api/ invitations` is role-keyed in the
141+ service: admins read the platform-global list; any other caller reads only the
142+ invitations they sent (` invitedBy` -scoped — the ` { invitedBy: 1 }` index covers
143+ it; a caller with no resolvable id gets ` []` , never the ` invitedBy: null `
144+ admin-created rows). CASL still grants the route to admins only — widening the
145+ ` Invitation` abilities to regular users is the referral phase's flip; the scoping
146+ ships first so that flip can never leak invitee emails (PII) platform-wide.
147+ 2. **Self-referral guard — SHIPPED (#3833), with a known residual**: ` create ()`
148+ rejects 422 "You cannot invite yourself" when the invitee email equals the
149+ inviter's own, before the E9 registered-email check. The grant-side floor
150+ (` expectedGrantKeys` drops the referrer side when
151+ ` invitedBy === acceptedUserId` — pinned in the billing unit tests) covers
152+ same-account pairs only. **Alias/variant self-invites (a second personal email
153+ → a separate account) are NOT prevented** — accepted residual risk; revisit
154+ (fraud review / email-normalization dedup) before any paid-rewards launch.
155+ 3. **Open-signup hole — DOCUMENTED, intentionally NOT changed**: claim/finalize stay
156+ gated on ` ! config .sign .up ` (the "open signup never burns a token" invariant).
157+ **Referral rewards therefore require ` sign .up : false ` .** On an open-signup
158+ deployment a presented token is *resolved* but never *claimed/finalized*, so
159+ ` invitation .accepted ` never fires and no grant occurs — enabling
160+ ` billing .referral ` there is a silent no-op. The Vue Referrals tab reads
161+ ` GET / api/ auth/ config` (` sign .up ` ) and replaces the invite form with an
162+ informational state when signup is open (the referrals list stays read-only).
1471634. **Index ` referredBy` ** alongside the first real referral query.
148164
149165## UI
0 commit comments