feat(nip-fi): admin disconnect/deny API with in-memory deny-until-TTL map (S4)

Adds POST /api/nip-fi/disconnect, an issuer-scoped deny-until-TTL map shared by WS and HTTP admission, and REQ/search effect permits so a denied session cannot register or read after its deadline.

Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
Duncan
2026-09-28 16:49:08 -04:00
parent f1e50be410
commit f1d93e0168
49 changed files with 24896 additions and 583 deletions

View File

@@ -192,6 +192,122 @@ run_unit_tests() {
run_test_step "buzz-relay admission regression test" \
cargo test -p buzz-relay --lib state::tests::neither_a_confirmed_inactive_community_nor_a_failed_lookup_admits_the_socket -- --nocapture
# NIP-FI (S3/S4) relay witnesses: mirrors the nextest stanza in `just
# test-unit` — the two lists must stay in step. Split into two steps:
#
# 1. Wholly-new modules (nip_fi_* and api::nip_fi) selected by substring
# filter. Filters go after `--`; libtest ORs multiple filters after `--`.
# 2. Sixty-eight exact mixed-module names (state, connection, audio::handler,
# router, handlers::*, config) passed with --exact so each filter is an
# exact match. The exact list keeps main's unselected tests in those
# modules out of this step; they include the ~30s sqlx acquire timeout
# tests that don't belong in the infra-free unit job.
#
# Postgres-backed NIP-FI witnesses run in the PostgreSQL lane, not here.
run_test_step "buzz-relay NIP-FI module tests" \
cargo test -p buzz-relay --lib -- --nocapture \
nip_fi_config:: nip_fi_gate:: nip_fi_session:: nip_fi_upgrade:: api::nip_fi::
# NIP-FI mixed-module exact-name list. The array is defined here so the
# guard and the run step share one authoritative list. Each entry must match
# a test that exists in buzz-relay --lib; a renamed or deleted entry silently
# matches nothing under libtest, so a discovery guard checks the count before
# running. Keep this list in step with the `just test-unit` nextest stanza
# (Justfile:478-548).
NIP_FI_EXACT_NAMES=(
audio::handler::tests::admin_disconnect_nip_fi_delivers_restricted_json_then_policy_close
audio::handler::tests::audio_expiry_sends_exact_restricted_frame_before_close
audio::handler::tests::b1_already_expired_session_denied_at_pairing_before_admission
audio::handler::tests::b1_mid_admission_expiry_does_not_add_peer_to_room
audio::handler::tests::cw6_guard_release_before_commit_calls_directory_release_exactly_once
audio::handler::tests::cw7_guard_release_before_commit_sends_clean_close_on_remote_stream
audio::handler::tests::handle_active_audio_connection_pairing_mismatch_runs_full_audio_denial_path
audio::handler::tests::pre_send_loop_check_cancel_emits_restricted_json_then_policy_close
audio::handler::tests::w8_membership_check_barrier_fires_before_db_read
audio::handler::tests::w_admin_disconnect_at_deny_check_delivers_payload_then_close
audio::handler::tests::w_audio_deny_absent_key_passes_deny_check_reaches_membership_gate
audio::handler::tests::w_audio_deny_active_key_refused_at_post_registration_check
audio::handler::tests::w_audio_deny_straddle_entry_inserted_between_registration_and_check_is_caught
config::tests::hermetic_for_test_is_deterministic
connection::tests::b2_cancelled_connection_event_frame_not_dispatched
connection::tests::b3_denial_precedes_restart_when_denial_already_won
connection::tests::b3_expiry_denial_precedes_close_through_send_loop
connection::tests::b3_root_pairing_denial_precedes_close_through_send_loop
connection::tests::deadline_exp_is_earliest_selects_exp
connection::tests::deadline_max_connection_lifetime_is_earliest_selects_partition
connection::tests::deadline_no_lifetime_returns_upstream_only
connection::tests::expiry_notice_queued_on_ctrl_before_cancel
connection::tests::r1_denial_precedes_restart_reason_won_frame_arrives_during_recv
handlers::auth::tests::b2_pre_cancelled_connection_never_becomes_authenticated
handlers::auth::tests::handle_auth_pairing_mismatch_runs_full_root_denial_path
handlers::event::tests::w_observer_permit_cancel_before_acquisition_blocks_publication
router::tests::b4_connection_upgrade_only_no_upgrade_header_not_gated
router::tests::b4_upgrade_only_no_connection_header_not_gated
router::tests::deny_map_admits_key_not_in_map
router::tests::deny_map_blocks_ws_admission_for_live_entry
router::tests::nip_fi_enforce_audio_denies_missing_assertion_401
router::tests::nip_fi_enforce_audio_denies_token_when_no_verifier_503
router::tests::nip_fi_enforce_nip11_content_negotiation_serves_200_not_401
router::tests::nip_fi_enforce_root_denies_missing_assertion_401
router::tests::nip_fi_enforce_root_denies_token_when_no_verifier_503
router::tests::nip_fi_enforce_ws_upgrade_with_html_accept_is_gated_401
state::tests::auth_wins_reason_enqueues_frame_then_losing_delete_does_not
state::tests::community_disconnect_then_nip_fi_keeps_community_deleted_reason
state::tests::conn_manager_disconnect_nip_fi_ignores_unproven_connection
state::tests::conn_manager_disconnect_nip_fi_is_issuer_scoped
state::tests::conn_manager_disconnect_nip_fi_sets_authorization_denied_reason
state::tests::delete_wins_reason_losing_auth_does_not_enqueue_frame
state::tests::delete_wins_reason_losing_expiry_does_not_enqueue_frame
state::tests::delete_wins_reason_losing_manager_does_not_enqueue_frame
state::tests::delete_wins_reason_losing_pairing_does_not_enqueue_frame
state::tests::disconnect_community_wins_reason_losing_nip_fi_does_not_enqueue_frame
state::tests::disconnect_nip_fi_wins_reason_enqueues_frame_then_losing_delete_does_not
state::tests::drain_all_jittered_cancels_when_restart_channel_is_full_or_closed
state::tests::drain_all_jittered_waits_for_writer_acknowledgement_without_cancelling
state::tests::expiry_wins_reason_enqueues_frame_then_losing_delete_does_not
state::tests::lifecycle_cancel_does_not_enqueue_frame_but_cancels_token
state::tests::manager_wins_reason_enqueues_frame_then_losing_delete_does_not
state::tests::nip_fi_disconnect_audio_is_issuer_scoped
state::tests::nip_fi_disconnect_closes_proven_audio_socket_and_sends_policy_close_reason
state::tests::nip_fi_disconnect_closes_target_audio_only_and_preserves_collocated_peer
state::tests::nip_fi_disconnect_does_not_close_different_pubkey_audio_socket
state::tests::nip_fi_disconnect_does_not_close_unproven_audio_socket
state::tests::nip_fi_disconnect_then_community_keeps_authorization_denied_reason
state::tests::pairing_wins_reason_enqueues_frame_then_losing_delete_does_not
state::tests::w_audio_registry_lifecycle_cancel_race_payload_precedes_audio_teardown_cancel
state::tests::w_auth_cancel_race_payload_precedes_community_cancel
state::tests::w_cancel_race_deny_payload_precedes_community_cancel
state::tests::w_expiry_cancel_race_payload_precedes_community_cancel
state::tests::w_lifecycle_cancel_race_payload_precedes_lifecycle_cancel
state::tests::w_lifecycle_cancel_race_reverse_manager_loses_after_lifecycle_wins
state::tests::w_manager_cancel_race_payload_precedes_community_cancel
state::tests::w_pairing_cancel_race_payload_precedes_community_cancel
state::tests::w_root_manager_drain_race_payload_precedes_drain_lifecycle_cancel
)
# Guard: every name in the array must resolve to exactly one test. A stale
# name silently matches nothing under libtest, so check the listed count
# equals the array length before running. Fails the summary if any name is
# missing or mistyped.
local _listed
_listed=$(cargo test -p buzz-relay --lib -- --list --exact \
"${NIP_FI_EXACT_NAMES[@]}" 2>/dev/null | grep -c ': test$' || true)
if [[ "${_listed}" -ne "${#NIP_FI_EXACT_NAMES[@]}" ]]; then
error "buzz-relay NIP-FI exact-name guard FAILED: listed ${_listed}/${#NIP_FI_EXACT_NAMES[@]} tests; a name is stale or missing"
FAILED+=("buzz-relay NIP-FI exact-name guard")
else
log "buzz-relay NIP-FI exact-name guard: ${_listed}/${#NIP_FI_EXACT_NAMES[@]} names resolved"
run_test_step "buzz-relay NIP-FI mixed-module tests" \
cargo test -p buzz-relay --lib -- --nocapture --exact \
"${NIP_FI_EXACT_NAMES[@]}"
fi
# buzz-pubsub unit tests (NIP-FI disconnect fan-out codec): infra-free,
# mirrors the nextest path in `just test-unit` — the two lists must stay
# in step.
run_test_step "buzz-pubsub unit tests" \
cargo test -p buzz-pubsub --lib -- --nocapture
}
# ---- DB / integration tests (infra required) --------------------------------