fix(invites): run the unknown-code check inside the admitted transaction

989412f3 ran the pre-lock `SELECT EXISTS` on the bare pool before
`begin_community_write_transaction`. That answered claims on fenced
(quiescing/deleting) tenants with `invite_invalid` instead of the fence
rejection, and acquired a writer connection outside a typed
`WriterOperation`. Open the admitted transaction first, which enforces
the tenant fence and attribution without taking the invite admission
lock, then check the code on `tx.conn()` before `lock_admission`.

Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Luke Tornquist <tornquist@squareup.com>
This commit is contained in:
Luke Tornquist
2026-10-28 15:35:41 -04:00
parent c16538dced
commit 36103818be

View File

@@ -423,21 +423,6 @@ pub async fn claim_relay_invite_with_owner(
verified_owner: Option<&str>,
relay_keypair: &nostr::Keys,
) -> Result<MembershipWrite<InviteClaimResult>> {
// Unknown codes are free to forge, so reject them before taking the
// community-wide admission lock; the locked read below stays authoritative.
let known: bool = sqlx::query_scalar(
"SELECT EXISTS (SELECT 1 FROM relay_invites \
WHERE community_id = $1 AND token_hash = $2)",
)
.bind(community.as_uuid())
.bind(token_hash)
.fetch_one(pool)
.await?;
if !known {
log_claim_outcome(community, None, "invalid", None, None);
return Ok(MembershipWrite::unchanged(ClaimOutcome::Invalid.into()));
}
let mut tx = crate::begin_community_write_transaction(
pool,
community,
@@ -445,6 +430,23 @@ pub async fn claim_relay_invite_with_owner(
)
.await?;
// Unknown codes are free to forge, so reject them after the tenant fence
// but before the community-wide admission lock; the locked read below
// stays authoritative.
let known: bool = sqlx::query_scalar(
"SELECT EXISTS (SELECT 1 FROM relay_invites \
WHERE community_id = $1 AND token_hash = $2)",
)
.bind(community.as_uuid())
.bind(token_hash)
.fetch_one(tx.conn())
.await?;
if !known {
tx.rollback().await?;
log_claim_outcome(community, None, "invalid", None, None);
return Ok(MembershipWrite::unchanged(ClaimOutcome::Invalid.into()));
}
lock_admission(tx.conn(), community).await?;
if claim_owner::claimant_restricted(tx.conn(), community, claimer_pubkey, verified_owner)
.await?