Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fc0b1ca
Add durable live member authorization checks
bradseiler Oct 5, 2026
c4dfcea
refactor(relay): address revalidation review
bradseiler Oct 6, 2026
abb6bfc
test(relay): bind durable revocation to socket admission
bradseiler Oct 9, 2026
1d633b9
test(relay): retain root lifecycle registration during revalidation
bradseiler Oct 9, 2026
da5a3ab
test(relay): import root fixture shared ownership type
bradseiler Oct 9, 2026
b4d6481
test(relay): measure live revocation without Redis delivery
bradseiler Oct 9, 2026
d704dfc
test(relay): bound live workflow handshake and control reads
bradseiler Oct 9, 2026
c2b5259
test(relay): classify live revalidation as external network infrastru…
bradseiler Oct 10, 2026
00fc8e3
test(relay): admit live agents through owners before revalidation
bradseiler Oct 10, 2026
109edb0
fix(desktop): retry transient relay auth failures
bradseiler Oct 12, 2026
d9c1846
fix(relay): prioritize stale authorization targets
bradseiler Oct 12, 2026
8fff217
fix(auth): log captured owner membership failures
bradseiler Oct 12, 2026
15af4d0
fix(relay): preserve token-owner admission during revalidation
bradseiler Oct 30, 2026
599a59f
test(relay): run live revalidation contracts in the unit lane
bradseiler Oct 30, 2026
70c9f66
fix(relay): preserve refusal accounting across live admission
bradseiler Nov 4, 2026
d7afb40
test(relay): run live revalidation accounting regression in CI
bradseiler Nov 4, 2026
d155b31
fix(db): bound live authorization statements and cancellation
bradseiler Nov 4, 2026
2900b9d
fix(relay): stagger authorization outage disconnects
bradseiler Nov 4, 2026
8500e37
style(db): format rebased authorization exports
bradseiler Nov 6, 2026
d91fe8c
fix(relay): log unavailable live authorization disconnects
bradseiler Nov 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,103 @@ A `CancellationToken` coordinates shutdown across all three loops.

Slow clients: `ConnectionState::send()` uses `try_send` — if the send buffer is full, a grace counter increments. After `SLOW_CLIENT_GRACE_LIMIT` (3) consecutive full-buffer events, the connection is cancelled. A successful send resets the counter.

### Live Community Authorization Revalidation

Authenticated root and huddle/audio sockets retain their server-resolved
community, principal, admission-captured owner, admission route, whether that
owner was a relay member at admission, and whether closed-relay membership was
granted through that owner. A relay-local worker checks
these records against batched writer-authoritative snapshots every 5 seconds. It
keeps the immediate local disconnect and Redis fan-out paths; this scan is the
durable backstop when a pod misses fan-out.

The maximum authorization-staleness budget is 30 seconds. A database failure
may preserve the last successful decision for 5 seconds plus a stable per-socket
offset below 9 seconds, measured from the
start of that session's last successful authoritative check; repeated failures
never extend the cutoff. The offset comes from the server-generated connection
UUID, spreading outage closes across scans without a timer per socket. The worker handles up to 500 distinct
`(community, principal, captured owner)`
targets per query, runs at most 4 queries at a time, gives each query 1 second,
and starts one shared 5-second lookup deadline before it snapshots and groups
the live-session registry. No database batch starts after that deadline;
unresolved targets are treated as lookup failures. Closing decisions are then applied in one
bounded sweep over the connection snapshot. Each query runs in a transaction used only for the read with a server-side statement
limit of at most 900 ms, below the shared 1-second client deadline; a tighter
operator limit is preserved. Rollback restores the writer session's limit.
Cancelled or failed lookups close the checked-out connection under SQLx's
bounded close path rather than waiting on its return-to-pool ping. The server
limit also stops work when PostgreSQL has not yet noticed a disconnect.
The conservative staleness bound is 5 seconds of base grace, 9 seconds of jitter,
one 5-second cadence, two 5-second scan windows, and one 1-second query deadline:
30 seconds, matching the budget. The relay's shared
`BUZZ_MAX_CONNECTIONS` semaphore caps root and audio sockets together at
10,000 by default; that is at most 20 batches and five query waves. The worker
creates no per-socket timers or per-connection revalidation queries. A scan
that exceeds its deadline treats unreturned targets as lookup failures and
closes their sessions when their existing grace has expired. If a result batch
is incomplete or malformed, every target in
that batch is treated as a lookup failure; no partial rows refresh
authorization.

Membership is required only when `require_relay_membership` is enabled. An
open-relay nonmember remains allowed unless directly or through its current
recorded owner it has an active community ban. An owner-derived member remains
allowed only while the relay, owner-delegation setting, and current owner
membership still permit that path. Removal of the admission-captured owner
revokes its session even if the agent's stored owner changed; captured-owner
membership never grants access to a directly admitted principal. The first
stored owner link also invalidates sessions admitted without one, matching the
existing reconnect path when NIP-OA materializes ownership; the durable check
detects that transition if its disconnect fan-out is missed. A directly
admitted member with a stable owner link still relies on its own membership,
and a change between already-recorded owners alone does not create a new
membership requirement for that principal. An active ban on the captured owner
also revokes that session. On open relays, an owner who was already a nonmember
at admission does not become a membership requirement. Moderation timeouts are
not read by this worker because they restrict writes, not an established
socket. All membership, ownership, and ban predicates include the bound
community. When the delegation check did not already establish the captured
owner's membership, admission makes a writer-authoritative membership
read to record whether that owner was already a relay member. This adds a writer
dependency to admission; a failed point read refuses the socket with an
authorization-unavailable AUTH response. The baseline distinguishes a later
membership removal from pre-existing open-relay nonmembership.

Each batch snapshot is the revalidation ordering point. A removal or ban
committed before that snapshot closes the session; a re-add or unban committed
before it keeps the session authorized. A later concurrent change is observed
on the next scan, and a close based on an earlier snapshot can be recovered by
reconnecting against current state. Admission binds the authenticated identity
before its final writer read and only publishes an admitted live record after
that check, so admission racing a removal is either cancelled by the immediate
path or rejected by the final read / next durable scan.

If the writer remains unavailable past the grace, the relay fails closed and
cancels the socket. NIP-FI root and audio sockets receive their existing
route-specific `authorization_denied` or `authorization_unavailable` terminal
response; other sockets receive the normal access-revoked or unavailable close
reason. This trades connection availability during a prolonged database
outage for a bounded authorization window. The desktop leaves explicit
server-side policy-read failures unauthenticated and retries them with its
existing jittered exponential reconnect schedule, capped at 30 seconds. A
successful later admission restores the connection without manual preconnect;
`restricted:` and `blocked:` responses remain terminal, and invalid or unknown
AUTH evidence retains its three-rejection cap. The worker shares graceful
shutdown cancellation and has no migration or rollout flag; all relay pods
must run this code for the bound to hold deployment-wide.

Targets are ordered within each scan by the oldest last-successful-check time
among their sessions, then by community, principal, and captured owner. A target
missed at a deadline therefore gets priority on the next scan while it remains
live. Under sustained writer saturation, deadlines can still leave targets
unresolved. Each unresolved socket closes at its own grace-plus-offset cutoff.
The five-second scan cadence spreads closes over two or three sweeps rather
than continuously. Compared with the former ten-second grace, a six-to-ten-second
blip can now close some sockets; the stable offsets reduce the peak reconnect
load while preserving the thirty-second bound. Confirmed revocations still
close immediately when an authoritative snapshot denies access.

### Step 5: Cleanup

On disconnect (any cause):
Expand Down
3 changes: 3 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,7 @@ test-unit:
+ test(=state::tests::nip_fi_disconnect_does_not_close_unproven_audio_socket)
+ test(=state::tests::community_disconnect_then_nip_fi_keeps_community_deleted_reason)
+ test(=state::tests::enforce_end_is_recorded_only_by_the_winning_terminal)
+ test(=state::tests::live_revalidation_does_not_record_deny_set_revocation)
+ test(=state::tests::disconnect_community_wins_reason_losing_nip_fi_does_not_enqueue_frame)
+ test(=state::tests::manager_disconnect_sets_reason_enqueues_frame_then_cancels)
+ test(/^api::nip_fi::/)'
Expand All @@ -661,6 +662,8 @@ test-unit:
cargo nextest run -p buzz-relay --lib -E 'test(=state::tests::admitted_index_follows_every_key_change) + test(/^shared_reads::tests::/) + test(/^membership_cache::tests::/) + test(/^community_active::tests::/) + test(=state::tests::concurrent_open_channel_misses_share_the_in_flight_read) + test(=state::tests::concurrent_restriction_and_host_misses_share_the_in_flight_read) + test(=tenant::tests::app_state_resolver_serves_fresh_cached_hosts_only) + test(=tenant::tests::an_overlong_host_fails_closed_before_the_resolver) + test(=state::tests::disconnect_drops_the_cached_restriction_row)'
# Retain partially consumed mesh receives across housekeeping ticks.
cargo nextest run -p buzz-relay --lib -E 'test(=mesh_boot::tests::demo_echo_retains_pending_receive_across_housekeeping_ticks)'
# Durable session policy, fairness, timeout and scale contracts are infra-free.
cargo nextest run -p buzz-relay --lib -E 'test(/^state::live_authorization_tests::/)'
# boot_lifecycle spawns the real relay binary and asserts its startup
# lifecycle, including that buzz_startup_phase_* reaches /metrics. Its
# non-ignored tests need no Postgres or Redis; the Postgres cases are
Expand Down
12 changes: 8 additions & 4 deletions crates/buzz-db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,11 @@ pub(crate) use runtime::{
pub use store::{
admin_moderation, allowlist, api_token, archived_identities, artifact, artifact_sync, channel,
channel_members, community, community_auth, deletion, dm, entity_deletion, event, feed,
git_repo, leader_lock, media, membership_snapshot, moderation, nip_fi_binding,
operator_listener, partition, personal_read, product_feedback, push, reaction, read_state,
relay_admin_actions, relay_invite, relay_members, relay_operators, reminder, replaceable,
storage_accounting, thread, thread_window, user, user_deletion, user_deletion_fences, workflow,
git_repo, leader_lock, live_authorization, media, membership_snapshot, moderation,
nip_fi_binding, operator_listener, partition, personal_read, product_feedback, push, reaction,
read_state, relay_admin_actions, relay_invite, relay_members, relay_operators, reminder,
replaceable, storage_accounting, thread, thread_window, user, user_deletion,
user_deletion_fences, workflow,
};

pub use allowlist::AllowlistEntry;
Expand All @@ -110,6 +111,9 @@ pub use error::{DbError, Result};
pub use event::{ChannelHeadPrecondition, ChannelHeadWriteStatus, DeletionTarget};
pub use event::{EventQuery, DEFAULT_MAX_PAGE_LIMIT};
pub use leader_lock::SessionLeader;
pub use live_authorization::{
LiveAuthorizationState, LiveAuthorizationTarget, LIVE_AUTHORIZATION_QUERY_TIMEOUT,
};
pub use nip_fi_binding::{NipFiBindingRecord, NipFiUnbind, RowCapExceeded};
pub use reaction::ReactionEventInsertOutcome;
pub use reminder::DueReminder;
Expand Down
Loading
Loading