Skip to content

feat(invitations): claim invite token at open signup when userFacing is on - #3982

Merged
PierreBrisorgueil merged 5 commits into
masterfrom
feat/3981-invitations-claim-open-signup
Jul 24, 2026
Merged

feat(invitations): claim invite token at open signup when userFacing is on#3982
PierreBrisorgueil merged 5 commits into
masterfrom
feat/3981-invitations-claim-open-signup

Conversation

@PierreBrisorgueil

@PierreBrisorgueil PierreBrisorgueil commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • What changed: when invitations.userFacing is true, the signup flow now claims and finalizes a presented invite token even while public signup is open — invitation.accepted, the analytics redemption event, referral reward, and referrer notification fire exactly as in closed-signup mode. userFacing: false (the default) keeps today's behavior unchanged, byte-for-byte. Also exposes invitations.userFacing in GET /api/auth/config (same top-level, unauthenticated pattern as sign.up) so a consumer can gate referral UI on it. The eligibility checker (the only code that calls claim()) now relays a claimed boolean on its opaque result so auth.controller.js trusts a single source of truth instead of re-deriving the same condition from config on both sides.
  • Why: the open-signup gate resolved a presented invite token but never claimed/finalized it while public signup was open, so on open-signup deployments the referral loop could never convert — invitations went out, rewards never landed.
  • Related issues: Closes ✨ Invitations: claim invite token at signup when userFacing, even with open signup + expose flag in auth config #3981

Scope

  • Module(s) impacted: auth (signup controller, eligibility JSDoc), invitations (eligibility checker, config comment, README)
  • Cross-module impact: none (the existing generic registerSignupEligibility seam — auth still never imports invitation code)
  • Risk level: low (change is behind an existing config flag defaulting to false; default-path behavior is unchanged, covered by a byte-for-byte regression test)

Validation

  • npm run lint
  • npm test (npm run test:unit — 2245 tests, npm run test:integration — 499 tests, npm run test:e2e — 16 tests, all green)
  • Manual checks done (if applicable) — full unit + integration matrix for {userFacing on/off} × {signup open/closed} × {token valid/absent/foreign-email}, plus a real-DB end-to-end pass proving single-use, referredBy, and the invitation.accepted event all fire in the new cell

Guardrails check

  • No secrets or credentials introduced (.env*, secrets/**, keys, tokens)
  • No risky rename/move of core stack paths
  • Changes remain merge-friendly for downstream projects
  • Tests added or updated when behavior changed

Notes for reviewers

  • Security considerations: all existing token guards stay enforced unconditionally (email pin, single-use via the atomic claim CAS, expiry, the create()-time self-referral guard) — this PR only widens WHEN the existing claim/finalize path runs, never how it validates a token. A signup without a token never invents attribution in either flag state.
  • Mergeability considerations: none — additive, config-gated, default-off behavior change plus a passthrough config exposure.
  • Follow-up tasks (optional):
    • The OAuth signup path's eligibility check is still skipped entirely while public signup is open (unrelated to this issue, which scoped to the token-based local-signup path only — no OAuth invite exists in that state today to claim).
    • 🐛 signup analytics invited:true doesn't distinguish presented-vs-attributed invites #3983: the user_signed_up analytics event's invited/invitationId/invitedBy fields key off a resolved invite, not off actual attribution (eligibility.claimed) — a narrow, pre-existing gap this PR's own claim-race downgrade slightly widens (found in independent review, filed rather than expanding this PR's scope).

…en userFacing is on

Closes #3981

The open-signup gate (#3833) always resolved a presented invite token but
never claimed/finalized it while public signup was open, so the referral
loop could never convert on open-signup deployments — `invitation.accepted`
never fired and no reward was granted.

Extend the claim/finalize/release gate from "signup was closed" to "signup
was closed OR invitations.userFacing is on" in both the eligibility checker
(invitations.init.js) and the signup controller (the new `inviteHonored`
derivation in auth.controller.js) — the two conditions are read from the
same config and must mirror exactly, since a mismatch would either finalize
an invite that was never claimed or leave a claimed one stuck. All existing
token guards (email pin, single-use, expiry, self-referral) stay enforced
unchanged; a signup without a token still never invents attribution.
`userFacing: false` (the default) preserves today's behavior byte-for-byte.

Also expose `invitations.userFacing` in GET /api/auth/config (same
top-level, unauthenticated pattern as `sign.up`) so a consumer can gate
referral UI on it.
…e claim condition in auth

Pre-push review (Phase 0 gate) flagged a high-severity risk: auth.controller.js
and invitations.init.js each independently re-derived the same closed-signup /
userFacing condition from config to decide whether an invite needed
finalize/release. A future edit to one side without the other would either
finalize an invite that was never claimed, or leave a claimed one stuck.

invitations.init.js is the only code that calls claim() — make it the single
source of truth by relaying a `claimed` boolean on the opaque eligibility
result. auth.controller.js now trusts `eligibility.claimed` verbatim instead
of re-deriving the condition from `config.invitations.userFacing`. Behavior
is unchanged; only the internal wiring moved.
Phase 0 gate iteration-2 nit: the JSDoc example on assertSignupEligible still
described the pre-#3981 { invite, finalize, release } shape.
@PierreBrisorgueil PierreBrisorgueil added the Feat A new feature label Jul 24, 2026
@PierreBrisorgueil PierreBrisorgueil self-assigned this Jul 24, 2026
@PierreBrisorgueil PierreBrisorgueil added the Feat A new feature label Jul 24, 2026
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@PierreBrisorgueil, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 8 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1ebe66f4-33ff-49a5-b257-428ad10c7fd6

📥 Commits

Reviewing files that changed from the base of the PR and between 223ca9e and 4d29cf8.

📒 Files selected for processing (10)
  • modules/auth/controllers/auth.controller.js
  • modules/auth/services/auth.eligibility.js
  • modules/auth/tests/auth.config.controller.unit.tests.js
  • modules/auth/tests/auth.signup.inviteHonored.unit.tests.js
  • modules/auth/tests/auth.silent.catch.unit.tests.js
  • modules/invitations/README.md
  • modules/invitations/config/invitations.development.config.js
  • modules/invitations/invitations.init.js
  • modules/invitations/tests/invitations.init.userFacing.unit.tests.js
  • modules/invitations/tests/invitations.integration.tests.js
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/3981-invitations-claim-open-signup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.28%. Comparing base (223ca9e) to head (4d29cf8).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3982      +/-   ##
==========================================
+ Coverage   93.15%   93.28%   +0.13%     
==========================================
  Files         170      170              
  Lines        5639     5647       +8     
  Branches     1816     1817       +1     
==========================================
+ Hits         5253     5268      +15     
+ Misses        311      306       -5     
+ Partials       75       73       -2     
Flag Coverage Δ
integration 61.39% <84.61%> (+0.09%) ⬆️
unit 75.50% <100.00%> (+0.28%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 223ca9e...4d29cf8. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

codecov/patch flagged these two release call sites as newly-uncovered
(the diff rewrote their guarding condition to inviteHonored, so the whole
line counts as added) — no prior test in the suite exercised a claimed
invite alongside a verify-step or org-provisioning throw.
@PierreBrisorgueil
PierreBrisorgueil marked this pull request as ready for review July 24, 2026 22:05
Fallback critical review (CodeRabbit rate-limited) caught a regression: on
userFacing open-signup, a lost claim() race (e.g. a double-submit / client
retry of the same invite link) threw AppError(422) out of the eligibility
checker, hard-failing the entire signup — even though open signup's own
invariant is that a presented token must never be able to block an
otherwise-valid signup. Pre-#3981, this same race on open signup never
called claim() at all, so it never blocked.

Scope the throw-on-claim-failure behavior to the closed-signup branch only
(where the invite is genuinely required, so a lost race legitimately blocks
signup — unchanged). The userFacing open-signup branch now catches a claim
failure and downgrades to unclaimed, letting signup proceed exactly as if
the token had merely been presented-but-not-required.

Also refreshes two stale auth.controller.js comments (still described the
pre-#3981 { invite, finalize, release } contract and claimed open signup
"never burns/locks a presented token" unconditionally) flagged in the same
review pass.
@PierreBrisorgueil

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 8 minutes.

@PierreBrisorgueil
PierreBrisorgueil merged commit 00768d0 into master Jul 24, 2026
8 checks passed
@PierreBrisorgueil
PierreBrisorgueil deleted the feat/3981-invitations-claim-open-signup branch July 24, 2026 22:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ Invitations: claim invite token at signup when userFacing, even with open signup + expose flag in auth config

1 participant