Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ad6529a
feat(buzz-relay): implement NIP-FI stateless enforcement (S3)
Sep 2, 2026
db6ef34
test(buzz-relay): fix env-race in nip_fi_config tests, add three-term…
Sep 2, 2026
2743d6a
fix(nip-fi): wire stateless enforcement — C1-C2 + I3-I7
Sep 2, 2026
9922bf4
fix(nip-fi): F1/F2/F3 — audio partition, JWKS supervisor, falsifiable…
Sep 2, 2026
6eef170
fix(nip-fi): F1/F2/F3 — audio partition, JWKS supervisor, falsifiable…
Sep 2, 2026
6602684
test(nip-fi): fix F3 witness falsifiability gaps from Paul's verify pass
Sep 2, 2026
bdde08d
chore(nip-fi): post-S2-rebase — drop require_attested_key arg + stale…
Sep 2, 2026
7d87ab3
fix(nip-fi): review round B1-B5 + C1-C7 behavioral fixes and cleanups
Sep 2, 2026
23f4385
fix(nip-fi): B1-B5 pass-2 correction round — admission fence, frame g…
Sep 2, 2026
503230c
fix(nip-fi): B1–B5 pass-3 correction round — admission gate, router o…
Sep 3, 2026
acaa6e9
Merge remote-tracking branch 'origin/main' into hayt/nip-fi-stateless…
Sep 3, 2026
b13fe83
fix(nip-fi): B1–B5 pass-3 gap-round — real barrier witnesses, joined …
Sep 3, 2026
0f17f23
test(relay): implement W9/W10/reaffirm real-DB witnesses + W2 persist…
Sep 3, 2026
d495387
test(relay): implement CW5/CW5-variant/CW8/CW10 contract witnesses + …
Sep 3, 2026
dd1e97f
fix(nip-fi): address pass-3 IMPORTANT 1–6 + witness rebuilds (CW6/CW7…
Sep 3, 2026
1093e7f
chore: merge origin/main into hayt/nip-fi-stateless-enforcement (hudd…
Sep 3, 2026
82a21a6
fix(nip-fi): address corrective round 2 residuals (I1/I3/I4/CW5v/CW6/…
Sep 3, 2026
cd5d414
fix(nip-fi): address corrective round 3 invariants (I1/I4/W2/CW6)
Sep 3, 2026
19fd134
fix(relay): transfer lease after commit-won; fix W2 oracle DB URL; dr…
Sep 3, 2026
3e77a2e
Merge remote-tracking branch 'origin/main' into hayt/nip-fi-stateless…
Sep 3, 2026
e2230e7
fix(nip-fi): close P1-a, P1-b, P2 security findings (corrective round 5)
Sep 3, 2026
6f27a9e
Merge remote-tracking branch 'origin/main' into hayt/nip-fi-stateless…
Sep 3, 2026
b7e9808
fix(nip-fi): close IMPORTANTs 1-3 from recheck-5 (corrective round 6)
Sep 3, 2026
9f0acac
fix(nip-fi): fix clippy::unusual_byte_groupings in P2 community UUID …
Sep 3, 2026
16536ac
fix(nip-fi): address review findings F1–F5
Sep 4, 2026
b187288
fix(nip-fi): address review findings F1–F5 (round 2)
Sep 4, 2026
37042db
fix(nip-fi): address pre-push review items (round 2 fixup)
Sep 4, 2026
9f21162
fix(nip-fi): close F1 scheduling witness and remove empty_for_test()
Sep 4, 2026
658af55
fix(nip-fi): correct F1 test oracle comments to match actual assertions
Sep 4, 2026
0aa732e
fix(nip-fi): close F1 witness defects — supervisor wiring oracle and …
Sep 4, 2026
4de38d1
chore(buzz-auth): remove unused attempt_count from ToggleJwksFetcher
Sep 4, 2026
2e6e904
fix(tests): read BUZZ_TEST_DATABASE_URL in audio_test_state_real_db
Sep 4, 2026
488c73f
Merge remote-tracking branch 'origin/main' into hayt/nip-fi-stateless…
Sep 4, 2026
52b2cd1
fix(nip-fi): address Carl/Thufir blocking review findings F1-F8
Sep 8, 2026
e7d7158
test(nip-fi): complete Gap A/B/C witness coverage for F1–F8
Sep 8, 2026
a9e091a
Merge remote-tracking branch 'origin/main' into hayt/nip-fi-stateless…
Sep 8, 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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/buzz-auth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ pub use access::MockAccessChecker;
#[cfg(any(test, feature = "test-utils"))]
pub use nip98_replay::AlwaysFreshReplayGuard;
#[cfg(any(test, feature = "test-utils"))]
pub use nip_fi::ToggleJwksFetcher;
#[cfg(any(test, feature = "test-utils"))]
pub use rate_limit::AlwaysAllowRateLimiter;

/// How the connection was authenticated.
Expand Down
42 changes: 42 additions & 0 deletions crates/buzz-auth/src/nip_fi/assertion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,48 @@ impl fmt::Debug for VerifiedAssertion {
}
}

#[cfg(any(test, feature = "test-utils"))]
impl VerifiedAssertion {
/// Test-only factory for building `VerifiedAssertion` fixtures without
/// going through the full JWT/JWKS verification path. NOT available in
/// production builds.
///
/// # Panics
///
/// Panics when `authority_deadlines` is empty — an empty set violates the
/// non-empty invariant that `upstream_authority_deadline()` relies on.
pub fn for_test(
asserted_key: Option<PublicKey>,
authority_deadlines: Vec<DateTime<Utc>>,
) -> Self {
assert!(
!authority_deadlines.is_empty(),
"VerifiedAssertion::for_test: authority_deadlines must be non-empty \
(upstream_authority_deadline() panics on empty)"
);
use super::config::{AssertionPolicyId, TransportContractId};
Self {
identity: FederatedIdentity {
issuer: "test-issuer".to_string(),
subject: "test-subject".to_string(),
},
asserted_key,
capabilities: CanonicalCapabilities::from_pairs(vec![]),
authority_deadlines,
assertion_policy_id: AssertionPolicyId::zero(),
transport_contract_id: TransportContractId::zero(),
revalidation_dependencies: RevalidationDependencies {
verification_key_id: "test-kid".to_string(),
key_snapshot_generation: 0,
key_snapshot_hard_deadline: DateTime::<Utc>::MAX_UTC,
confidential_assertion: ConfidentialAssertion {
compact_jws: "test.test.test".to_string(),
},
},
}
}
}

impl RevalidationDependencies {
pub(super) fn new(
verification_key_id: String,
Expand Down
14 changes: 14 additions & 0 deletions crates/buzz-auth/src/nip_fi/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ impl AssertionPolicyId {
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}

/// Zero value for tests. Available in production builds only with the
/// `test-utils` feature enabled.
#[cfg(any(test, feature = "test-utils"))]
pub const fn zero() -> Self {
Self([0u8; 32])
}
}

impl fmt::Debug for AssertionPolicyId {
Expand Down Expand Up @@ -144,6 +151,13 @@ impl TransportContractId {
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}

/// Zero value for tests. Available in production builds only with the
/// `test-utils` feature enabled.
#[cfg(any(test, feature = "test-utils"))]
pub const fn zero() -> Self {
Self([0u8; 32])
}
}

impl fmt::Debug for TransportContractId {
Expand Down
69 changes: 69 additions & 0 deletions crates/buzz-auth/src/nip_fi/jwks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -734,5 +734,74 @@ impl<F> std::fmt::Debug for ProductionJwksSource<F> {
}
}

/// A toggle-controlled [`JwksFetcher`] for use in downstream-crate integration
/// tests. Returns a minimal but valid JWKS document when the toggle is `true`,
/// and `NetworkError` when `false`.
///
/// This is the only path by which a crate outside `buzz-auth` can build a
/// `ProductionJwksSource` with a controllable fetch outcome — `sealed::Sealed`
/// is crate-private, so downstream crates cannot implement `JwksFetcher`
/// directly. Because the returned JWKS is real (not a synthetic shortcut), the
/// full `ProductionJwksSource` code path — parse, bound, cache, hard-deadline —
/// exercises itself normally, and the resulting `AssertionKeySet` is valid for
/// verifier lookups.
///
/// Only available with the `test-utils` feature enabled.
#[cfg(any(test, feature = "test-utils"))]
#[derive(Clone, Debug)]
pub struct ToggleJwksFetcher {
/// When `true` the fetcher returns a minimal valid JWKS body; when `false`
/// it returns `JwksFetchError::NetworkError`.
pub available: std::sync::Arc<std::sync::atomic::AtomicBool>,
/// Fired (via `notify_one`) after every fetch attempt completes, whether
/// successful or not. Tests can await this to deterministically observe
/// that the fetch loop executed a given attempt before proceeding.
pub fetch_done: std::sync::Arc<tokio::sync::Notify>,
}

#[cfg(any(test, feature = "test-utils"))]
impl ToggleJwksFetcher {
/// Construct a new `ToggleJwksFetcher`. Pass `initial` as the starting
/// availability state; `available` and `fetch_done` are externally
/// observable and can be driven from the test after construction.
pub fn new(initial: bool) -> Self {
Self {
available: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(initial)),
fetch_done: std::sync::Arc::new(tokio::sync::Notify::new()),
}
}
}

#[cfg(any(test, feature = "test-utils"))]
impl super::verifier::sealed::Sealed for ToggleJwksFetcher {}

#[cfg(any(test, feature = "test-utils"))]
impl JwksFetcher for ToggleJwksFetcher {
fn fetch_jwks<'a>(
&'a self,
_uri: &'a str,
) -> impl std::future::Future<Output = Result<String, JwksFetchError>> + Send + 'a {
// A minimal P-256 JWK. The coordinates are the same values used in
// buzz-auth's own test suite (tests.rs `minimal_jwks_json`).
const TOGGLE_JWKS: &str = concat!(
r#"{"keys":[{"kty":"EC","crv":"P-256","#,
r#""x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU","#,
r#""y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0","#,
r#""use":"sig","alg":"ES256","kid":"toggle-kid"}]}"#
);
let available = self.available.load(std::sync::atomic::Ordering::SeqCst);
let fetch_done = std::sync::Arc::clone(&self.fetch_done);
async move {
let result = if available {
Ok(TOGGLE_JWKS.to_string())
} else {
Err(JwksFetchError::NetworkError)
};
fetch_done.notify_one();
result
}
}
}

#[cfg(test)]
mod tests;
3 changes: 3 additions & 0 deletions crates/buzz-auth/src/nip_fi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,8 @@ pub use jwks::{
HttpJwksFetcher, IssuerJwksConfig, JwksFetchError, JwksFetcher, JwksSourceContract,
ProductionJwksSource,
};

#[cfg(any(test, feature = "test-utils"))]
pub use jwks::ToggleJwksFetcher;
pub use startup::{validate_nip_fi_config, NipFiMode, NipFiStartupError};
pub use verifier::{AssertionKeySet, FederatedAssertionVerifier, IssuerKeySource, VerifierError};
10 changes: 10 additions & 0 deletions crates/buzz-db/src/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,16 @@ impl Db {
}
}

/// Return a reference to the writer pool.
///
/// Callers that need a pool handle for standalone free functions (e.g.,
/// `buzz_db::insert_mentions`) can use this. Prefer the `Db` method
/// equivalents when they exist; use `pool()` only for functions that have
/// no `Db` wrapper yet.
pub fn pool(&self) -> &PgPool {
&self.pool
}

/// Refresh all expected operation-specific waiter gauges, including zero.
///
/// The relay pool sampler calls this periodically so an exporter idle
Expand Down
76 changes: 76 additions & 0 deletions crates/buzz-db/src/store/channel_members.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,82 @@ async fn acquire_channel_membership_lock(
Ok(())
}

// ── Transaction-level membership helpers (for commit_participant_join) ────────

/// Acquire the per-channel membership advisory lock on a caller-owned transaction.
///
/// Equivalent to the internal `acquire_channel_membership_lock`, but exposed
/// for callers that need to compose multiple operations in one transaction
/// (e.g., `commit_participant_join` in `audio/handler.rs`).
pub async fn acquire_channel_membership_lock_in_transaction(
tx: &mut Transaction<'_, Postgres>,
community_id: CommunityId,
channel_id: Uuid,
) -> Result<()> {
acquire_channel_membership_lock(tx, community_id, channel_id).await
}

/// Check whether a pubkey is an active channel member on a caller-owned transaction.
///
/// Runs the same query as `is_member` but within the caller's transaction so
/// the read is serialized with any concurrent membership writes on the same lock.
pub async fn is_member_in_transaction(
tx: &mut Transaction<'_, Postgres>,
community_id: CommunityId,
channel_id: Uuid,
pubkey: &[u8],
) -> Result<bool> {
let row = sqlx::query(
"SELECT COUNT(*) as cnt FROM channel_members cm \
JOIN channels c ON cm.community_id = c.community_id AND cm.channel_id = c.id AND c.deleted_at IS NULL \
WHERE cm.community_id = $1 AND cm.channel_id = $2 AND cm.pubkey = $3 AND cm.removed_at IS NULL",
)
.bind(community_id.as_uuid())
.bind(channel_id)
.bind(pubkey)
.fetch_one(&mut **tx)
.await?;
let cnt: i64 = row.try_get("cnt")?;
Ok(cnt > 0)
}

/// Auto-add a member on a caller-owned transaction (for ephemeral-channel admission).
///
/// Inserts or reactivates the membership row at `Member` role with the given
/// `invited_by` (channel creator for huddle auto-add). Does NOT acquire the
/// advisory lock — callers must have already called
/// `acquire_channel_membership_lock_in_transaction` before calling this.
///
/// Used by `commit_participant_join` to atomically add membership and the
/// `48101` event in a single transaction under a session effect permit.
pub async fn insert_auto_membership_in_transaction(
tx: &mut Transaction<'_, Postgres>,
community_id: CommunityId,
channel_id: Uuid,
pubkey: &[u8],
invited_by: &[u8],
) -> Result<()> {
sqlx::query(
r#"
INSERT INTO channel_members (community_id, channel_id, pubkey, role, invited_by)
VALUES ($1, $2, $3, 'member'::member_role, $4)
ON CONFLICT (community_id, channel_id, pubkey) DO UPDATE SET
removed_at = NULL,
removed_by = NULL,
role = EXCLUDED.role
"#,
)
.bind(community_id.as_uuid())
.bind(channel_id)
.bind(pubkey)
.bind(invited_by)
.execute(&mut **tx)
.await?;
Ok(())
}

// ── End transaction-level helpers ─────────────────────────────────────────────

/// An active member roster captured while holding the channel's membership
/// serialization lock on one writer connection.
pub struct LockedMemberSnapshot {
Expand Down
Loading
Loading