mirror of
https://github.com/block/buzz.git
synced 2026-10-28 13:01:10 +00:00
Keep v1 natural drain with an optional UTC cutoff. Serialize claim and mint policy reads with restriction and owner writes. Share historical revocation repair across migration and desired-state upgrades while retaining deletion fences. Co-authored-by: Codex <codex@openai.com> Signed-off-by: Brad Seiler <seiler@squareup.com> Signed-off-by: Local Builder <1c476cb1127ea270cb900bdc3891060140bdd1173f91498030ee9b4db01c2f20@buzz.block.builderlab.xyz>
122 lines
5.6 KiB
PL/PgSQL
122 lines
5.6 KiB
PL/PgSQL
ALTER TABLE relay_invites ADD COLUMN revoked_at TIMESTAMPTZ;
|
|
|
|
-- Serialize invitation admission with restriction and ownership changes per tenant.
|
|
CREATE FUNCTION invite_admission_lock(community UUID) RETURNS VOID
|
|
LANGUAGE sql AS $$
|
|
SELECT pg_advisory_xact_lock(hashtextextended('invite-admission:' || community::text, 0));
|
|
$$;
|
|
|
|
CREATE FUNCTION lock_invite_restrictions() RETURNS TRIGGER
|
|
LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
PERFORM invite_admission_lock(NEW.community_id);
|
|
RETURN NEW;
|
|
END
|
|
$$;
|
|
|
|
-- Revocation is durable: neither unban nor expiry restores an issued bearer.
|
|
CREATE FUNCTION revoke_banned_issuer_invites() RETURNS TRIGGER
|
|
LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
IF NEW.banned AND (NEW.ban_expires_at IS NULL OR NEW.ban_expires_at > clock_timestamp()) THEN
|
|
UPDATE relay_invites SET revoked_at = clock_timestamp()
|
|
WHERE community_id = NEW.community_id AND revoked_at IS NULL
|
|
AND (created_by = encode(NEW.pubkey, 'hex') OR created_by IN (
|
|
SELECT encode(pubkey, 'hex') FROM users
|
|
WHERE community_id = NEW.community_id AND agent_owner_pubkey = NEW.pubkey
|
|
));
|
|
END IF;
|
|
RETURN NEW;
|
|
END
|
|
$$;
|
|
|
|
-- Learning a verified owner must not revive invites issued by its restricted agent.
|
|
CREATE FUNCTION revoke_restricted_agent_invites() RETURNS TRIGGER
|
|
LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
IF NEW.agent_owner_pubkey IS NOT NULL AND EXISTS (
|
|
SELECT 1 FROM community_bans WHERE community_id = NEW.community_id
|
|
AND pubkey = NEW.agent_owner_pubkey AND banned
|
|
AND (ban_expires_at IS NULL OR ban_expires_at > clock_timestamp())
|
|
) THEN
|
|
UPDATE relay_invites SET revoked_at = clock_timestamp()
|
|
WHERE community_id = NEW.community_id AND created_by = encode(NEW.pubkey, 'hex')
|
|
AND revoked_at IS NULL;
|
|
END IF;
|
|
RETURN NEW;
|
|
END
|
|
$$;
|
|
|
|
CREATE TRIGGER invite_restriction_lock BEFORE INSERT OR UPDATE ON community_bans
|
|
FOR EACH ROW EXECUTE FUNCTION lock_invite_restrictions();
|
|
CREATE TRIGGER invite_issuer_revocation AFTER INSERT OR UPDATE ON community_bans
|
|
FOR EACH ROW EXECUTE FUNCTION revoke_banned_issuer_invites();
|
|
|
|
CREATE TRIGGER invite_owner_lock BEFORE INSERT OR UPDATE OF agent_owner_pubkey ON users
|
|
FOR EACH ROW EXECUTE FUNCTION lock_invite_restrictions();
|
|
CREATE TRIGGER invite_owner_revocation AFTER INSERT OR UPDATE OF agent_owner_pubkey ON users
|
|
FOR EACH ROW EXECUTE FUNCTION revoke_restricted_agent_invites();
|
|
|
|
-- Include active bans and recorded prior bans, so lifting a ban before this
|
|
-- migration cannot restore an invite issued before that ban. Scope every owner
|
|
-- and audit lookup to the invitation's community.
|
|
-- Schema migrations hold the exclusive schema-destruction lock. Retain the
|
|
-- universal tenant fence too: acquire its shared lock, then read the current
|
|
-- generation before applying this narrowly scoped historical security repair.
|
|
-- A quiescing tenant may return to active, so skipping it could revive bearers.
|
|
CREATE FUNCTION backfill_invite_revocations() RETURNS BIGINT
|
|
LANGUAGE plpgsql AS $$
|
|
DECLARE
|
|
target RECORD;
|
|
affected BIGINT;
|
|
total BIGINT := 0;
|
|
current_generation BIGINT;
|
|
prior_community TEXT := current_setting('buzz.deletion_executor_community', true);
|
|
prior_generation TEXT := current_setting('buzz.deletion_fence_generation', true);
|
|
BEGIN
|
|
FOR target IN SELECT id FROM communities
|
|
WHERE EXISTS (SELECT 1 FROM relay_invites WHERE community_id = communities.id)
|
|
ORDER BY id
|
|
LOOP
|
|
PERFORM pg_advisory_xact_lock_shared(community_deletion_lock_key(target.id));
|
|
SELECT deletion_fence_generation INTO STRICT current_generation
|
|
FROM communities WHERE id = target.id;
|
|
PERFORM set_config('buzz.deletion_executor_community', target.id::text, true);
|
|
PERFORM set_config('buzz.deletion_fence_generation', current_generation::text, true);
|
|
UPDATE relay_invites i SET revoked_at = clock_timestamp()
|
|
WHERE i.community_id = target.id AND i.revoked_at IS NULL AND EXISTS (
|
|
SELECT 1 FROM (
|
|
SELECT CASE WHEN i.created_by ~ '^[0-9a-f]{64}$'
|
|
THEN decode(i.created_by, 'hex') END AS pubkey
|
|
UNION ALL
|
|
SELECT agent_owner_pubkey FROM users
|
|
WHERE community_id = i.community_id AND encode(pubkey, 'hex') = i.created_by
|
|
AND agent_owner_pubkey IS NOT NULL
|
|
) principal
|
|
WHERE EXISTS (
|
|
SELECT 1 FROM community_bans b WHERE b.community_id = i.community_id
|
|
AND b.pubkey = principal.pubkey AND b.banned
|
|
AND (b.ban_expires_at IS NULL OR b.ban_expires_at > clock_timestamp())
|
|
) OR EXISTS (
|
|
SELECT 1 FROM moderation_actions a WHERE a.community_id = i.community_id
|
|
AND a.target_pubkey = principal.pubkey AND a.action IN ('ban', 'resolve:ban')
|
|
AND a.created_at >= i.created_at
|
|
) OR EXISTS (
|
|
SELECT 1 FROM relay_admin_actions a WHERE a.report_community_id = i.community_id
|
|
AND a.enforcement_target_pubkey = principal.pubkey AND a.action = 'ban'
|
|
AND (a.step_marker = 'mutation_committed' OR a.state = 'succeeded')
|
|
AND a.updated_at >= i.created_at
|
|
)
|
|
)
|
|
;
|
|
GET DIAGNOSTICS affected = ROW_COUNT;
|
|
total := total + affected;
|
|
PERFORM set_config('buzz.deletion_executor_community', COALESCE(prior_community, ''), true);
|
|
PERFORM set_config('buzz.deletion_fence_generation', COALESCE(prior_generation, ''), true);
|
|
END LOOP;
|
|
RETURN total;
|
|
END
|
|
$$;
|
|
|
|
SELECT backfill_invite_revocations();
|