Skip to content

feat(invites): add use-limited invite links - #3141

Merged
kalvinnchau merged 8 commits into
mainfrom
kalvin/limit-invite-links
Jul 27, 2026
Merged

feat(invites): add use-limited invite links#3141
kalvinnchau merged 8 commits into
mainfrom
kalvin/limit-invite-links

Conversation

@kalvinnchau

Copy link
Copy Markdown
Contributor

Summary

  • add database-backed v2 invite links with optional maximum-use limits and atomic final-slot redemption
  • preserve v1 invite compatibility while adding exhausted/expired/invalid client handling across desktop, web, and mobile
  • emit structured claim-outcome logs with community, invite ID, outcome, maximum uses, and post-claim count

Verification

  • cargo fmt --all -- --check
  • cargo test -p buzz-db (85 passed, 134 Postgres-dependent ignored)
  • cargo clippy -p buzz-db --all-targets -- -D warnings
  • desktop npm run typecheck
  • push hook: desktop checks/tests, desktop Tauri tests, Rust tests, and branch-skew passed
  • Postgres integration tests were previously reviewed green at the pre-rebase tree; local rerun on this session was unavailable because Postgres/Docker were not running
  • mobile push-hook check could not start because Flutter is unavailable locally

@kalvinnchau
kalvinnchau requested a review from a team as a code owner July 27, 2026 16:13

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Combined review from two independent passes (deduped). Verified head: a4af9622d.

Overall: the core design is strong and both reviews converged on that — 32-byte opaque secrets with hash-only storage, tenant-scoped (community_id, token_hash) lookups, the full redemption (membership insert, policy evidence, use_count increment) in one transaction under SELECT ... FOR UPDATE, exact-prefix v2. routing with no v1 fallback, and NIP-43 side effects only on Joined. The same-member/different-invite race is handled correctly (ON CONFLICT ... DO NOTHING + rows_affected() returns the loser as AlreadyMember without consuming the invite). Test coverage of the concurrency and rollback contracts is unusually thorough — one reviewer ran all 5 ignored buzz-db::relay_invite tests and all 12 ignored buzz-relay::api::invites tests against isolated Postgres and everything passed.

Findings are inline: 1 IMPORTANT (CI never executes the tests that prove the atomic-redemption contract — independently verified by both reviewers against Justfile and ci.yml at this head), 1 IMPORTANT-but-follow-up-acceptable (no retention path for expired invite rows — flagged by both reviewers), and 4 minor/optional notes.

Severity tags per comment; nothing here questions the design itself.

Comment thread crates/buzz-db/src/relay_invite.rs
Comment thread migrations/0025_relay_invites.sql
Comment thread web/src/features/invite/ui/InvitePage.tsx Outdated
Comment thread crates/buzz-db/src/relay_invite.rs Outdated
Comment thread crates/buzz-db/src/relay_invite.rs
Comment thread crates/buzz-db/src/relay_invite.rs Outdated
Comment thread crates/buzz-relay/src/invite_token.rs
npub1c4alndp82zyt9veaklm5d965quss79vlhk9awv7qu5erwhmf42qqlvc25c and others added 8 commits July 27, 2026 14:59
- migration 0025_relay_invites.sql: relay_invites table with SHA-256
  token_hash, community-scoped PK and UNIQUE, max_uses CHECK 1-10000,
  use_count, role pinned to 'member'
- buzz-db relay_invite module: mint_relay_invite (32-byte secret,
  base64url, hash, persist) and claim_relay_invite (12-step atomic
  transaction: SELECT FOR UPDATE, expiry/membership/capacity checks,
  member insert, policy evidence, use_count increment, single commit)
- relay invites.rs: v2 prefix routing in claim_invite, ClaimOutcome
  mapped to HTTP responses (joined/already_member/expired/exhausted/
  invalid), NIP-43 side effects only on Joined
- relay invites.rs: mint_invite now calls db.mint_relay_invite and
  returns v2 response shape with max_uses/uses_remaining/expires_at
- invite_token.rs: V2_PREFIX constant for exact prefix routing
- schema.sql and migration.rs tests updated for new table (24->25)

Co-authored-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
…ion UX

- InviteLinkSection.tsx: removed stray </div>, added bounded vs unlimited
  Switch + max-uses Input with validation (1-10000), description text
- invites.ts: MintedInvite type includes maxUses/usesRemaining;
  mintInvite() accepts { ttlSecs?, maxUses? }
- useClaimInvite.ts: isInviteExhaustedError check mapping to exhaustion message
- inviteHelpers.ts: INVITE_EXHAUSTED_ERROR constant + isInviteExhaustedError()
- inviteHelpers.test.mjs: 4 tests for exhaustion sentinel
- invites.test.mjs: 6 tests covering bounded/unlimited serialization
- web InvitePage.tsx: invite_exhausted mapped to dedicated message
- mobile invite_join_provider.dart: exhaustion handling in
  _friendlyInviteError() and _requiresFreshInvite()
- mobile invite_join_provider_test.dart: new test for exhaustion path

Desktop: 3452 tests pass, 0 failures. tsc --noEmit passes.
Co-authored-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Co-authored-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Co-authored-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Co-authored-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
@kalvinnchau
kalvinnchau force-pushed the kalvin/limit-invite-links branch from 98f79b2 to d2d76de Compare July 27, 2026 22:04
@kalvinnchau
kalvinnchau merged commit d500c2d into main Jul 27, 2026
32 checks passed
@kalvinnchau
kalvinnchau deleted the kalvin/limit-invite-links branch July 27, 2026 22:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants