feat(invites): add use-limited invite links - #3141
Conversation
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 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.
- 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>
98f79b2 to
d2d76de
Compare
Summary
Verification
cargo fmt --all -- --checkcargo test -p buzz-db(85 passed, 134 Postgres-dependent ignored)cargo clippy -p buzz-db --all-targets -- -D warningsnpm run typecheck