Skip to content

Commit 08119cc

Browse files
docs(invitations): referral requires closed signup; #3833 gate status
Documents the open-signup invariant kept on purpose: with sign.up true a presented token is resolved but never claimed/finalized, so invitation.accepted never fires and referral grants are a silent no-op. Marks gates 1-2 shipped in the README and mirrors the caveat in the billing referral config block. refs #3833
1 parent ada3214 commit 08119cc

2 files changed

Lines changed: 31 additions & 11 deletions

File tree

modules/billing/config/billing.development.config.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,12 @@ const config = {
140140
* Pair with the reconcile cron (crons/billing.referralReconcile.js): the listener
141141
* is in-process fire-and-forget (latency); the cron back-fills missed grants (truth).
142142
*
143-
* ⚠️ Merging is safe everywhere (default OFF); do NOT enable until
144-
* pierreb-devkit/Node#3833 lands — only the cheap self-referral floor ships here.
143+
* ⚠️ Referral grants require CLOSED signup (sign.up: false). With public signup
144+
* open, a presented invite token is resolved but never claimed/finalized (the
145+
* "open signup never burns a token" invariant), so `invitation.accepted` never
146+
* fires and enabling this block is a silent no-op. The #3833 gates shipped:
147+
* role-keyed list scoping + the create() self-invite guard (the grant-side
148+
* floor here skips invitedBy === acceptedUserId as the belt).
145149
*/
146150
referral: {
147151
enabled: false,

modules/invitations/README.md

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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).
147163
4. **Index `referredBy`** alongside the first real referral query.
148164
149165
## UI

0 commit comments

Comments
 (0)