Repository navigation
fix(relay): fail startup on invalid operator listener config - #7933
Merged
jsibbison-square merged 1 commit intoSep 28, 2026
Merged
Conversation
🔐 Codex Security Review
|
jsibbison-square
force-pushed
the
jsib-260928-fail-fast-invalid-listener-config
branch
from
September 28, 2026 04:06
67b105a to
f665574
Compare
Signed-off-by: Implementor <691cca7a870db1dad6990d5938d1b4a2a2ca9647ca81290b536c17b06b2e473f@buzz.block.builderlab.xyz>
jsibbison-square
force-pushed
the
jsib-260928-fail-fast-invalid-listener-config
branch
from
September 28, 2026 04:45
f665574 to
26b6ae7
Compare
jsibbison-square
marked this pull request as ready for review
September 28, 2026 05:42
TheSentinel454
approved these changes
Sep 28, 2026
jsibbison-square
deleted the
jsib-260928-fail-fast-invalid-listener-config
branch
September 28, 2026 13:18
wpfleger96
pushed a commit
that referenced
this pull request
Sep 28, 2026
Co-authored-by: Will Pfleger <[email protected]> Signed-off-by: Will Pfleger <[email protected]> * origin/main: fix(relay): fail startup on invalid operator listener config (#7933) fix(db): limit event transactions to listener mention kinds (#7932) feat(relay): deliver pubkey mentions to relay companions (#7793) docs(protocol): propose simplified channel artifacts (#7791) feat(push): support configurable HTTP(S) delivery URLs (#7877) fix(ci): select runtime suites from PR changes only (#7843) test(desktop): synchronize upload edit smoke test (#7903) Signed-off-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
wpfleger96
pushed a commit
that referenced
this pull request
Sep 28, 2026
* origin/main: 🤖 docs(nip-fi): remove implementation references from the spec (#7912) fix(relay): fail startup on invalid operator listener config (#7933) fix(db): limit event transactions to listener mention kinds (#7932) feat(relay): deliver pubkey mentions to relay companions (#7793) docs(protocol): propose simplified channel artifacts (#7791) feat(push): support configurable HTTP(S) delivery URLs (#7877) fix(ci): select runtime suites from PR changes only (#7843) test(desktop): synchronize upload edit smoke test (#7903) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
abipalli
pushed a commit
to abipalli/buzz
that referenced
this pull request
Oct 7, 2026
) Addresses point 3 of [previous comment](block#7793 (review)) "3. Fail fast on a bad BUZZ_OPERATOR_LISTENERS (config.rs:857)" ## Summary - Propagate malformed `BUZZ_OPERATOR_LISTENERS` values from `Config::from_env` as `ConfigError`, including invalid UTF-8. - Update startup config tests to assert returned errors and restore the original admin warning capture helper, removing the shared helper introduced only for listener log assertions. - Cover valid and absent listener values through `Config::from_env`. ## Context Follow-up to the review of block#7793 specifically [reverting some config logging parts](https://github.com/block/buzz/pull/7793/changes#diff-eaa0eeb209ff1ad6212ffa56d3a983bb732858647c9fb227ef7483a3363b5a71). Invalid listener configuration currently logs an error and silently disables listener delivery. ## Verification - `cargo fmt --all -- --check` passed with Hermit. Signed-off-by: Implementor <691cca7a870db1dad6990d5938d1b4a2a2ca9647ca81290b536c17b06b2e473f@buzz.block.builderlab.xyz> Co-authored-by: Implementor <691cca7a870db1dad6990d5938d1b4a2a2ca9647ca81290b536c17b06b2e473f@buzz.block.builderlab.xyz>
bse-ai
added a commit
to arcadia/buzz
that referenced
this pull request
Oct 20, 2026
* feat(push): support configurable HTTP(S) delivery URLs (#7877) The relay can use a configurable HTTP(S) gateway delivery URL and signs the same URL it sends. The gateway verifies NIP-98 against the received request URL, including authority, port, path and query. App Attest, scoped grants, replay checks and delivery quotas are unchanged. Forwarding proxies must preserve Host and supply the original request scheme through X-Forwarded-Proto. The NIP relay-delivery wording now defines the configured HTTP(S) URL as the signed URL and labels the public URL as an example. <!-- buzz-review-completed --> --------- Signed-off-by: Tom Brow <[email protected]> * docs(protocol): propose simplified channel artifacts (#7791) ## Summary Adds a short, proposal-only **NIP-AR: Channel Artifacts** in `docs/nips/NIP-AR.md`, as a simpler alternative to the closed #7771. - Defines complete-snapshot artifact events with stable identity and atomic, `prev`-based revisions. - Uses channel access and posting permissions for reads, edits, moves, and writer-authorized deletion. - Keeps client-defined content opaque to the relay, with exact tag filtering over current accessible state and bounded queries. - Covers durable source removal on moves, terminal deletion, and safe client fallbacks without defining task/project schemas or changing application behavior. ### Remaining design work This is a compact draft, not a complete implementation-ready wire contract. Concrete query request/pagination/error/limit-discovery formats, removal/replay wire details, and numeric query limits remain follow-up work. Prefer extending shared WebSocket `REQ` / HTTP `/query` surfaces rather than adding plugin-specific endpoints. No database layout or scale guarantees are proposed. ### Related issue Closest prior proposal: #7771 (closed). Searched existing PRs and issues for channel artifacts; no matching issue found. ### Testing - Verified the committed document is byte-for-byte identical to the reviewed local draft. - Parsed the example JSON and its embedded JSON content; checked unique two-element example tags. - Reviewed the entire one-file diff and ran `git diff --check`. - Commit and push hooks passed (including DCO sign-off and differential file-size checks). - Full application CI was not run locally: this changes only a Markdown proposal, with no implementation or UI changes. --------- Signed-off-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz> Signed-off-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz> Signed-off-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz> Co-authored-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz> Co-authored-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz> Co-authored-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz> * feat(relay): deliver pubkey mentions to relay companions (#7793) We are introducing a companion service to the relay that needs pubkey event mentions for pubkeys it manages. This PR introduces an outbox for pubkey mentions that will be sent to these "operator listener" services. ## Summary Deliver mentions of registered pubkeys to deployment-global operator listeners. - Configure listeners with `BUZZ_OPERATOR_LISTENERS`; listeners manage target pubkeys through NIP-98-authenticated `POST`/`DELETE /operator/listener/pubkeys` requests. - During supported event ingestion, match `p` tags to registrations and insert one outbox row per listener in the event transaction. - A background worker claims up to 10 rows, sends NIP-98-signed notifications, and retries transient failures with exponential backoff for up to 9 attempts (idle polling backs off from 250 ms to 2 s). - Outbox rows are ineligible for delivery after 15 minutes and are reaped every 5 minutes. Registered pubkeys live for 30 days; re-registering renews them, and a daily reaper removes expired registrations. - Keep persistence app-managed: no database triggers, functions, foreign keys, or separate match queue. ## Changed behavior Listener outbox rows are enqueued inside the shared event-insert and thread-metadata transactions; enqueue failure rolls back the event. `event_mentions` behavior is unchanged: ordinary event/reaction mention indexing remains post-commit best effort with warning-only failures. ## Testing - `cargo fmt --all` - `cargo test -p buzz-db --lib` - `cargo test -p buzz-relay --lib config::tests::operator_listener_routes` - `cargo check -p buzz-db -p buzz-relay` * fix(db): limit event transactions to listener mention kinds (#7932) ## Summary - Keep non-listener-mention event writes on the plain insert path without opening a transaction. - Preserve the transaction around listener mention event insertion and outbox enqueue. ## Validation - `cargo fmt --all -- --check` (Hermit) - Tests not run. Follow-up to Baxen’s review of #7793, point 1. Signed-off-by: Implementor <691cca7a870db1dad6990d5938d1b4a2a2ca9647ca81290b536c17b06b2e473f@buzz.block.builderlab.xyz> Co-authored-by: Implementor <691cca7a870db1dad6990d5938d1b4a2a2ca9647ca81290b536c17b06b2e473f@buzz.block.builderlab.xyz> * fix(relay): fail startup on invalid operator listener config (#7933) Addresses point 3 of [previous comment](https://github.com/block/buzz/pull/7793#pullrequestreview-5333444423) "3. Fail fast on a bad BUZZ_OPERATOR_LISTENERS (config.rs:857)" ## Summary - Propagate malformed `BUZZ_OPERATOR_LISTENERS` values from `Config::from_env` as `ConfigError`, including invalid UTF-8. - Update startup config tests to assert returned errors and restore the original admin warning capture helper, removing the shared helper introduced only for listener log assertions. - Cover valid and absent listener values through `Config::from_env`. ## Context Follow-up to the review of #7793 specifically [reverting some config logging parts](https://github.com/block/buzz/pull/7793/changes#diff-eaa0eeb209ff1ad6212ffa56d3a983bb732858647c9fb227ef7483a3363b5a71). Invalid listener configuration currently logs an error and silently disables listener delivery. ## Verification - `cargo fmt --all -- --check` passed with Hermit. Signed-off-by: Implementor <691cca7a870db1dad6990d5938d1b4a2a2ca9647ca81290b536c17b06b2e473f@buzz.block.builderlab.xyz> Co-authored-by: Implementor <691cca7a870db1dad6990d5938d1b4a2a2ca9647ca81290b536c17b06b2e473f@buzz.block.builderlab.xyz> * 🤖 docs(nip-fi): remove implementation references from the spec (#7912) This removes implementation references and implementation status from `docs/nips/NIP-FI.md` so the spec holds normative text only. It drops the Blossom compliance note, the known-gap notes, crate/file paths and PR numbers, roadmap "supersedes" sentences, doc-history lines, and the design-rationale note on session-only vs deny-until-TTL (its restart re-push SHOULD remains stated normatively elsewhere). Two wording changes carry normative weight: `PUT /media/upload` is listed as a plain kind-24242 upload route instead of a temporary alias, and the JWKS fetcher's SSRF controls become a MUST instead of a description. It doesn't depend on any enforcement PR and can merge at any time. Related: #7265 --------- Signed-off-by: Will Pfleger <[email protected]> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> * Make relay readiness process-local (#7341) ## Summary Make Kubernetes readiness depend only on the relay process lifecycle, so a shared Postgres or Redis slowdown cannot withdraw every pod at once. Move dependency checks to one fixed-cadence loop per pod. The loop permits only one evaluation at a time, emits metrics after each sample, and caches a timestamped report. `/_status` reads that cache without calling Postgres, Redis, or the deletion catalog. It reports `not_yet_sampled`, `fresh`, or `stale`. Each completed sample also writes a Unix timestamp gauge, so Datadog can compute sample age at query time. Fail closed when the community lifecycle lookup errors. An inconclusive tenant-state lookup no longer admits a socket to AUTH or REQ handling. ### Related issue None found. This addresses the elevated relay HTTP 500 and reconnect incident investigated on 2026-09-03. ### Tradeoff The readiness gauge now reports the latest private-probe observation, not the shutdown transition itself. After shutdown starts, a scrape can still see `1` until the next readiness probe refreshes the gauge to `0`. Kubernetes routing is unaffected because it uses the readiness response from the authoritative process state. A terminal `0` was never guaranteed: even a transition-owned gauge could update in process and then exit before Prometheus scraped it. If no scrape occurs before exit, both designs miss the terminal sample. The new design adds at most the interval until the next readiness probe when a scrape occurs during shutdown. Dependency status is now sampled every 30 seconds rather than on demand. A slow evaluation delays the next sample instead of stacking more work. The status payload exposes the cached report age. The completion timestamp stays unchanged when sampling stops, so `time() - timestamp` crosses the stale threshold without a server aging loop. Outcome counters and duration histograms remain cumulative and cannot provide no-data detection. ### Testing Gimli exercised exact SHA `edff1ec4a5362e132e367bf7449d2b0a68f1e9a9` on an isolated Blox stack. Repeated `/_status` bursts caused no dependency calls, metrics advanced without status traffic, stale status remained visible, and lifecycle lookup failures admitted no AUTH or REQ handling. Follow-up verification at `fdbb394ed598c94cde85ed01cb11e9de0ee8f00d` confirmed sampler-owned outcome and duration emission without status traffic and bound the real startup seam. The final timestamp-gauge change is covered by targeted red/green tests and the same boot regression at `c0f36e22b527c2fd6a63a548d036d006ffa17171`. ### Complexity Readiness remains one process-local sample per probe. Dependency I/O now has one owner and one execution path; request volume cannot increase dependency-check load. One completion timestamp replaces the prior server-side age update path; Datadog computes age without another loop. Community admission also returns to one fail-closed rule for inactive and unknown tenant state. Generated with [Claude Code](https://claude.ai/code) --------- Signed-off-by: tornquist <[email protected]> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Codex <[email protected]> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5 <[email protected]> Co-authored-by: Amp <[email protected]> * feat(nip-fi): harden Blossom kind-24242 verifier to NIP-FI spec (#7288) Brings `buzz-media`, `buzz-relay`, all first-party kind-24242 minters, and desktop token minting into compliance with [NIP-FI §kind-24242](https://github.com/block/buzz/blob/main/docs/nips/NIP-FI.md). ## What changed **`buzz-media/src/auth.rs`** - Add `BlossomStrictness` enum (`Strict` | `Permissive`). `Strict` applies full NIP-FI rules (active modes); `Permissive` preserves pre-NIP-FI Off-mode behavior byte-identical [FI-INV-15]. - Rewrite `verify_blossom_auth_event_for_verb` with count-based cardinality: exactly one `t`/`expiration`/`server` in `Strict`; duplicate `x` also rejected. `Permissive` keeps main's boolean semantics exactly: any valued `t` other than the verb (including `""`) rejects, valueless `t` is ignored. - `Strict`: mandatory `server` tag on all proofs (upload + read); absent or mismatched → `evidence_rejected` (`ServerMismatch`). - `Strict`: 60-second proof window (`now - created_at <= 60s`, `expiration <= created_at + 60s`). `Permissive`: 3600s / optional server. - **[F1]** Permissive server validation uses any-match semantics: a proof with multiple `server` tags is accepted if any tag matches the bound host. This restores origin/main base behavior. Strict duplicate-rejection unchanged. - **[F1 compat]** Permissive server gate uses `server_values.is_empty()` (valued tags only) rather than `server_count > 0` (includes valueless tags). A lone valueless `["server"]` tag is treated as absent in Permissive/Off mode, matching pre-NIP-FI base behavior [FI-INV-15]. - **[F4]** Valueless `x` tags in Strict get reads are tracked before `filter_map` so they cannot become absent scope (host-wide authorization). Any `x` tag present in Strict must contain the exact requested sha256. - **[F3 unit]** Cases A–D in `auth.rs` are helper-level unit tests: they call `verify_blossom_upload_auth` and `verify_upload_hash_only` directly. They do not enter the upload pipeline (`process_upload`/`process_video_upload`). Retained as helper evidence. **`buzz-media/src/error.rs`** - Add `DuplicateTag(&'static str)` variant. - Add `blossom_denial_kind()` method: classifies each auth error as `MissingEvidence`, `EvidenceRejected`, or `AuthorizationDenied`. Non-auth errors return `None`. - `IntoResponse` retains the legacy JSON 401 shape [FI-INV-15]. **`buzz-relay/src/api/media.rs`** - Add `MediaDenial(MediaError, BlossomStrictness)` wrapper mapping to byte-exact NIP-FI fixed responses in `Strict`; falls through to legacy JSON in `Permissive`. - `blossom_strictness_from_state()` selects `Strict` when `state.config.nip_fi.is_enforce()`, otherwise `Permissive`. Strictness is threaded through the `admit_nip_fi_http_on_state` NIP-98 closures on upload and read, and through the post-admission X-SHA-256, hash-binding, and membership denials. - `extract_blossom_auth` keeps first-value `Authorization` extraction; Enforce rejects repeated fields at the `admit_nip_fi_http` cardinality gate, and Off keeps legacy behavior [FI-INV-15]. - Both membership-denial call sites use `media_denial(RelayMembershipRequired, strictness)` so Strict mode produces the correct fixed-text response. - `upload_blob` serving-write fence conversion (`:406`) uses `map_err(|e| media_denial(e, strictness))` — a community fence committed between auth admission and lease acquisition no longer resets Strict to Permissive. - Mode-driven regressions in `mod postgres_tests` (selected by the nextest `postgres-ci` profile via `test(/postgres_tests::/)`, `#[ignore = "requires Postgres"]`), run through the full relay router with a real Enforce verifier and assertion: Enforce rejects Permissive-only proofs (300s lifetime, missing `server` tag) on both upload routes and reads with `evidence rejected`; Off accepts the same proofs with pre-NIP-FI responses; DenyProtected answers 503 before any proof check; membership denials are `authorization denied` text/plain in Enforce and exact legacy JSON 403 in Off, with a member positive control. GET and HEAD are each discriminated (HEAD with a suppressed body). Both upload routes pin the handler's `X-SHA-256` missing/malformed/unsigned and post-body hash-mismatch denials in both modes. Off rejects an empty-valued `t` beside a valid one with legacy 401 JSON in either order, and ignores a valueless one. Exact status, Content-Type, challenge, and body bytes are pinned. **`buzz-media/src/upload.rs`** (`mod minio_tests`) - **[F3 pipeline]** Four tests exercising the actual post-body call sites (`process_upload` `:85`, `process_video_upload` `:413`) with real MinIO I/O: buffered/streaming × expired-proof-accept + mismatched-hash-reject. The positive cases use `fresh_strict_upload_auth` (`created_at = now-58`, `expiration = now+2`, lifetime = 60s) — Strict-admissible at sign time — followed by a real 3s sleep so the proof is expired when the upload completes. `nostr::Timestamp::now()` reads the OS wall clock directly; paused Tokio time does not advance it. The negative cases use a pre-expired proof with a mismatched `x` tag. All four are `#[ignore = "requires MinIO"]`; no CI lane runs them automatically. **`buzz-media/src/validation.rs`** - `minimal_valid_mp4()` promoted to `#[cfg(test)] pub(crate)` — a 618-byte pre-computed H.264/avc1 fast-start MP4 available to all `buzz-media` test modules; smoke-tested by `test_minimal_valid_mp4_passes_validate_video_file`. **`desktop/src-tauri/src/commands/media.rs`** - `sign_blossom_upload_auth`: `server` tag mandatory; upload expiry 60s (was 3600s/300s). - `MEDIA_GET_AUTH_EXPIRY_SECS`: 60s (was 600s). **`desktop/src-tauri/src/media_proxy.rs`** - Single re-mint+retry on 401/403 for range requests; non-auth failures do not retry. **`crates/buzz-cli/src/client.rs`** - Read expiry: 60s (was 600s). Upload expiry: 60s. `server` tag mandatory on upload. **`crates/buzz-dev-mcp/src/view_image.rs`** - `MEDIA_GET_AUTH_EXPIRY_SECS`: 60s (was 600s). Test updated. **`mobile/lib/shared/relay/media_auth.dart`** - `_mediaGetAuthLifetimeSeconds`: 60 (was 600). Refresh margin updated. **`mobile/lib/shared/relay/media_upload.dart`** - `_uploadAuthLifetimeSeconds`: 60 (was 300). `server` tag mandatory. **`mobile/lib/features/channels/media_viewer_page/video_viewer.dart`** - **[F2]** Remove Android-specific `VideoPlayerController.networkUrl` streaming path. `video_player_android` 2.9.5 freezes headers into static `DefaultHttpDataSource` request properties — a 60s proof minted at controller creation time becomes stale on seeks. All platforms use the authenticated local-file download path. - **[F2 transport]** `unawaited(request.sink.close())` before `client.send()`. `AbortableStreamedRequest` carries no request body; without closing the sink, `IOClient.send()` awaits `stream.pipe(ioRequest)` which blocks until the sink ends — every video download hung in loading indefinitely on every platform. - **[F2 pending-ctrl]** `pendingController` ref set before the first `await (initialize())`, cleared in all exit paths. Effect cleanup disposes `pendingController.value` so a close-during-init releases the native player even if the initialized event never arrives. - **[F2r(a)]** On native init failure (post-create, pre-initialized-event), the inner catch starts disposal detached via `unawaited(localController.dispose().catchError(...))` — the `.catchError` handler absorbs any disposal exception so it does not reach the test error zone — then rethrows the original load error. `video_player` 2.11.1 completes the init future with an error without disposing the native player. - **[F2r(b)]** Non-2xx error body cancelled via `_cancelVideoResponse()` (subscribe+cancel) instead of `drain()`. `drain()` waits for the upstream to close; a stalled server body blocked `initializeVideo` indefinitely. - **[F2r(d)]** `unawaited(localController.dispose().catchError(...))` in the catch — not `await`. Two failure mechanisms exist: (1) when `createWithOptions()` throws, `_creatingCompleter` is never completed and `await dispose()` deadlocks at `:682-683`, blocking the outer catch from setting error state. (2) when `createWithOptions()` succeeds but initialization fails, `dispose()` runs but its thrown exception becomes an unhandled async error reaching the test zone. Both are fixed: `unawaited` lets the outer catch run immediately and set `error.value`; `.catchError(...)` on the detached disposal future absorbs any disposal exception. **`mobile/test/features/channels/media_viewer_page/video_viewer_test.dart`** - `_FinalizingFakeClient`: calls `request.finalize().drain()` — a test without the sink fix times out because the drain never completes, validating the transport contract. - F2r(a): `forceInitError=true` fake emits `PlatformException` on subscribe; `disposeCallCount >= 1` verifies disposal even on init failure. - F2r(b): `stalledBody` `StreamController` with `onCancel` Completer (bounded completion signal, not a sleep). Asserts: (1) `bodyStreamCancelled=true` while body stream is still open (impossible with `drain()`); (2) error UI visible while body remains open. Restoring `drain()` breaks both. - F2r(c): `neverInitialize=true` fake never sends initialized event; unmount asserts `disposeCallCount >= 1` via `pendingController`. Distinct from the event-error case. - F2r(d): `forceCreateError=true` fake throws `PlatformException` from `createWithOptions()` itself (before any player ID is returned). Asserts error UI visible within a bounded deadline. With the old `await dispose()` the outer catch is blocked — `error.value` is never set and the error UI assertion fails at the deadline; with `unawaited(dispose())` the error text appears immediately. **`mobile/test/features/channels/media_viewer_page/video_viewer_transport_test.dart`** (separate file) - Real-IO loopback probes using `flutter_test` (not `package:test`) to avoid `depend_on_referenced_packages` lint. No `TestWidgetsFlutterBinding` — no HttpClient override interfering with `dart:io`. - Probe 1: `HttpServer.bind(loopbackIPv4, 0)` reads the full request body before replying. Without `sink.close()`, `req.drain()` hangs and the test times out. - Probe 2: server signals request arrival via a Completer (not a sleep); abort fires after confirmed arrival; `client.send()` must throw `RequestAbortedException` (typed assertion) within a bounded 5s deadline. **`scripts/test-video-upload.sh`** - Proof minted with `+60` expiry and mandatory `server` tag. **`buzz-test-client/tests/e2e_media*.rs`** + **`buzz-relay/src/api/media.rs` test fixtures** - All fixtures updated to 55s expiry + `server` tag. **`RELEASING.md`** - New "NIP-FI upload proof freshness (relay floor)" section documenting that 60s proofs require the first relay release carrying this PR's post-body hash-only check (the first tag > `v0.2.1` — update once cut) for reliable large-file uploads. Operators on older self-hosted relays (≤ `v0.2.1`) must upgrade before receiving these clients. ## Finding 3: 60s upload proofs vs old-relay compatibility Desktop/CLI/mobile upload proofs are now 60s. Older relays re-run full expiry verification after the body is transferred. A valid upload whose body transfer takes >60s will fail on an old relay. **Resolution (relay floor prerequisite):** `RELEASING.md` requires that verified supported deployments (relay releases confirmed to carry the post-body hash-only check, commit `75e9bef748`) be in place before distributing 60s-proof client builds. Unknown or unverified self-hosted relay deployments are excluded from distribution until they upgrade. This makes the verified-deployment status a prerequisite for release distribution, not a post-distribution advisory. The concrete tag (`> v0.2.1`) is to be recorded once cut. ## Sequencing note Builds on #7264 (merged): Blossom strictness is derived from its `config.nip_fi` mode in this PR, so enabling `BUZZ_NIP_FI_MODE=enforce` activates the strict verifier with no further wiring. ## GIF compatibility exception An unauthenticated Off-mode request on a configured tenant with no GIF provider changes from 404 to 401 (base `gifs.rs:269-275` checked an unconfigured provider first; the current code authenticates first). All other Off-mode Blossom responses are byte-identical to pre-NIP-FI behavior. --------- Signed-off-by: Will Pfleger <[email protected]> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> * fix(sidebar): converge stale-at-open state across devices (sections/sort/stars/mutes) (#7805) Adds bounded-backoff recovery reads to all four sidebar sync hooks (sections, sort, stars and mutes). A stale relay read at app open now converges without a user edit or a reconnect. ## What this does **Shared recovery hook (`useStaleReaderRecovery`).** All four hooks use one retry effect. It reads immediately on mount, then at 5, 10, 30 and 60 s, staying at 60 s after that. A change of `visibilityState` to `visible` triggers an immediate read. Reads are single-flight. The effect restarts whenever `pubkey` or `relayUrl` changes, so a hook that stays mounted while switching relays drops the old relay's in-flight reads. **Guards that stop a recovery read from rolling back a local edit:** 1. **Skip the read while an edit is pending.** If an edit is pending when a tick starts, the tick skips its read and just schedules the next one. Without this, the read could return the pre-edit blob after the edit published and pending cleared. At that point neither apply-time guard could reject it. 2. **Pending check inside the state updater.** `hasPending()` and the effect lifetime are re-read inside `setStore(prev => ...)`. That way the check runs when React executes the updater, not when the response arrives. 3. **Local-mutation revision.** Each hook counts its local edits. The count is snapshotted when a read starts and checked again at apply time. If an edit landed while the read was in flight, the response is discarded. 4. **Head advanced only after a successful cache write.** A failed `write*Store()` leaves the same head retryable on the next tick. **Ownership-aware pending clear.** Each sync manager's `doPublish(store)` now clears `pendingStore` only while it still holds that same `store`, on both the identical-payload early return and the success path. Before, an older publish that finished after a newer edit had replaced `pendingStore` wiped the newer edit's pending marker, so a recovery read before the newer debounce could apply the older relay head over it. `destroy()` still clears unconditionally. ## Scope Reader-side recovery plus the ownership-aware pending clear. No outboxes, arbitration or replay changes. It applies to `useChannelSections`, `useChannelSortPreference`, `useChannelStars` and `useChannelMutes`. Related: [#7806](https://github.com/block/buzz/pull/7806) --------- Signed-off-by: Will Pfleger <[email protected]> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> * Add owner deletion admission control plane (#7818) ## Summary Adds the relay and database admission boundary for owner-requested deletion of archived hosted communities. The operator endpoint records accepted owner intent with stable UUID idempotency and immutable owner/operator provenance. Admission serializes with unarchive and owner transfer, and accepted requests disappear from actionable owner listings. This keeps destructive execution out of the request path. The existing durable deletion engine remains the only executor, so this change adds no lifecycle state or second deletion mechanism. Complexity stays bounded: one durable request is both the idempotency record and the mutation fence. ### Related issue None found. ### Testing - `REDIS_URL=redis://127.0.0.1:6379 BUZZ_POSTGRES_ADMIN_URL=postgres://buzz:buzz_dev@localhost:5432/postgres PGHOST=localhost PGPORT=5432 PGUSER=buzz PGPASSWORD=buzz_dev PG_BIN_DIR=/tmp/buzz-pg-bin ./scripts/postgres-test-run.sh -p buzz-db -p buzz-relay --lib --tests` — 385 passed. - `cargo test -p buzz-db` — passed. - `cargo fmt --all -- --check` and `cargo clippy -p buzz-db -p buzz-relay --all-targets -- -D warnings` — passed. - `just desktop-test desktop-build desktop-tauri-check desktop-tauri-test web-build` — passed; Desktop ran 6,589 tests. - Full `just test` could not start uncached Keycloak dependencies because `quay.io` reset connections. Full `just ci` reached Flutter dependency resolution, then `pub.dev` reset connections. Generated with Codex --------- Signed-off-by: tornquist <[email protected]> Signed-off-by: Codex <[email protected]> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Codex <[email protected]> Co-authored-by: Claude Opus 5 <[email protected]> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> * test(desktop): scope video menu e2e probes to emitted messages (#7953) ## Summary Fixes the `Desktop Smoke E2E (4)` failure on `main`. The test at `desktop/tests/e2e/video-attachment.spec.ts` ("right-click menus expose distinct selectors for links, relay video, and off-relay video") fails 3/3 attempts on main and on every PR branch since ~Sept 25. Because "Desktop" is a required check, that blocks merges. **Cause:** the relay-video and off-relay-video probes picked their player with `getByTestId("video-player").last()`. When mock messages share a second, timeline ordering can put a different video last. The locator then resolves to the wrong player, and the "Download video" menu detaches before the click, so the test times out. **Fix:** scope each probe to the `data-message-id` of the message `emitVideoMessage` just returned. Test-only change, 16 lines. ## Attribution This is a cherry-pick of `dfd5db4e0` by @tornquist, originally part of #7827 (the deletion-drain cron, which is stacked on another feature branch). It's pulled out on its own so main can go green without waiting on that stack. Authorship is preserved. ## Testing - `pnpm build:e2e && playwright test tests/e2e/video-attachment.spec.ts --project=smoke`: 13/13 passed locally, including the target test. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: tornquist <[email protected]> Signed-off-by: murderbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz> Co-authored-by: tornquist <[email protected]> Co-authored-by: Codex <[email protected]> * Add native HPKE encryption for nsec backups (#7849) ## Summary Add a Rust API that encrypts an nsec into a backup envelope using RFC 9180 Base mode: DHKEM(P-256, HKDF-SHA256), HKDF-SHA256, and AES-256-GCM. --------- Signed-off-by: jm <[email protected]> Signed-off-by: Jordan Mecom <[email protected]> Co-authored-by: Codex <[email protected]> * 🤖 perf: bound long-thread aux reads and make query deadlines terminal (#7854) Long threads (over ~100 replies) time out in desktop. The aux hop that loads reactions and other e-tag references builds its filter as one `tags @> …` term per id, joined with `OR`. Past about 100 ids the planner switches to walking each partition's primary key backward, expecting to stop early. When few rows actually match, it scans the whole community. With a dirty GIN pending list, a 188-reply thread never finishes. ### What changes - **Set-based e-tag reads (`buzz-db`).** The aux e-tag query now puts the id set behind a `MATERIALIZED` CTE and matches with `ANY`. Each e-tag read also runs under a transaction-local `statement_timeout` of 20 s. - **Terminal timeout contract (`buzz-relay`).** When a read hits the deadline, the client gets a terminal timeout (`CLOSED` for REQ, the timeout error for COUNT and search), not a retryable failure. - **Subscription lifecycle (`buzz-relay`).** The per-connection subscription map lock is now the lifecycle lock. Claims, replacement, client `CLOSE`, owner-checked timeout and search cleanup, revoke, and connection teardown all change the map, the registry, topic refcounts and terminal frames while holding it. As a result, a timed-out subscription's `CLOSED` goes out before any replacement with the same id can claim it, and claims made after teardown are refused. - **Dropped terminal frame disconnects (`buzz-relay`).** If the `CLOSED` for a retired subscription (owner-checked timeout or channel eviction) can't be queued, the relay cancels the connection instead of leaving the client holding a subscription that will never receive fan-out. A client-initiated `CLOSE` ack is exempt. - **COUNT under the same deadline (`buzz-db`).** The fast COUNT path runs e-tag counts under the same transaction-local 20 s cap as SELECT, so WS COUNT and HTTP `/count` produce the terminal timeout too. - **Replica reads surface the deadline (`buzz-db`).** When a replica-routed read is cancelled by `statement_timeout` (SQLSTATE `57014`), the error is returned instead of re-running the query on the writer, which would run the same slow statement. Other replica errors still fall back to the writer. `PLANS/REPLICA_FULL_READ_ROUTING_DESIGN.md` records the exception. - **Deadline handling (desktop).** Only the relay's `query timed out` counts as a query deadline. A client-side `request timed out` stays retryable. For one-shot reads a deadline is terminal: the global query retry (budget 1), thread-replies retry and reaction hydration don't re-send the same slow query. Reaction ids held back by a deadline are released when the channel is opened again, so reopening retries them while same-channel re-renders don't. Live subscriptions, in both the renderer and the native archive-sync socket, keep treating a deadline `CLOSED` as transient: they back off and resubscribe with the same filter instead of being dropped. - **Deadline handling (mobile).** Mobile recognizes the deadline on both transports: HTTP `503 {"error":"query timed out"}` and the WS `CLOSED` reason. - The root `ProviderScope` retry policy returns no retry for deadline errors and keeps Riverpod's default backoff for everything else. - Background thread-summary recounts don't retry a deadline. - Forum posts polling pauses while the last load settled on a deadline; reopening the forum retries. - A channel's newest-window load surfaces a deadline instead of falling back to legacy WS history, and the channel list's batched queries return "unavailable" on a deadline instead of fanning out to per-filter fallbacks. An unavailable batch is not treated as empty. Known last-message timestamps and unread state are kept, and unread catch-up moves `lastMessageAt` forward (never backward) the same way live events do, so a cold start with no known timestamp still gets one. - A live subscription that gets a deadline `CLOSED` backs off and resubscribes with the same filter. History requests still fail on it. - Settled load failures show a Retry control, matching desktop's thread-replies error card: a shared `LoadErrorView` on the channel timeline, forum thread and forum posts list, and an icon button beside the thread replies summary. While the thread query has no value, the thread page shows the route snapshot plus live, cached and optimistic replies, with the loading or error status still visible. - Other re-sends (reconnect, foreground resume, live events, the unread backstop, scroll) are not suppressed; each is bounded by the relay's 20 s statement deadline. - **Long-thread seeder.** `just seed-long-thread [replies]` posts one thread (default 187 replies) with some reactions to the local dev relay, for reproducing long threads in the apps. It uses a throwaway key, refuses non-loopback relays and ignores ambient `BUZZ_*` credentials. Writes are paced to fit the stock relay rate limit. **Lifecycle concurrency.** The lock serializes one connection's subscription changes. Other connections can still be delayed by pressure on the shared pubsub command queue, and by serial eviction loops waiting behind a blocked connection. This PR does not claim strict isolation between connections. **Recovery vs. load.** Clients stop automatic retries of the same one-shot read, but they don't remember deadlines across other triggers: reconnect, resume, live events and the unread backstop can each send the read again later, bounded by the 20 s statement deadline. A live subscription that hits the deadline resubscribes with its original filter under the existing 1–30 s backoff, and against a filter that keeps timing out it retries at the 30 s cap indefinitely, so the worst case is one bounded query per subscription every 30 s. `since` is deliberately not advanced to now on resubscribe, because that would skip events missed while the subscription was closed. Unchanged from `main`: when mobile's channel history sync fails with cached messages available, the cached messages are shown without the error or Retry control. ### Benchmark Local reproduction on an isolated bench stack (not committed): - PG16, migrations applied, one 188-reply thread plus 5M background events in the same community. - Each request replays the desktop thread-replies filter through the relay, with `include_aux` on. Times are total ms for 3 runs, with a 60 s client cap. GIN pending-list state is controlled with `VACUUM` (clean) or by refilling the pending list (dirty), and recorded per partition with `pgstatginindex`. Dirty runs held 402 pending pages on every partition's tags index at the start and end of each matrix. Clean runs held 0–1 pages (relay startup writes). | build | GIN pending state | limit 10 | 50 | 100 | 200 (188 ids) | |---|---|---|---|---|---| | `main` | clean | 26 / 16 / 13 | 56 / 51 / 52 | 92 / 93 / 95 | 662 / 639 / 641 | | `main` | dirty | 315 / 298 / 327 | 22141 / 15888 / 16769 | 28992 / 27819 / 28080 | timeout ×3 (>60 s) | | this PR | clean | 35 / 21 / 14 | 57 / 49 / 48 | 82 / 80 / 75 | 153 / 150 / 135 | | this PR | dirty | 290 / 254 / 251 | 1257 / 1204 / 1206 | 2432 / 2378 / 2382 | 4333 / 4706 / 5104 | Baseline `origin/main` `b37e47721`; this PR at `5f2650f5f`, whose e-tag read builders are identical to the current head. At head `1ad81d868` against `origin/main` `15d44dcce`, limit 200 measured 145–179 ms clean (`main` 683–715) and 4.41–4.51 s dirty (`main` timeout ×3). Every completed cell returns the same signed event set: 209 events, 187 replies at limit 200. With `include_aux` off, every cell is ≤66 ms. The thread-window loader (`select_aux`) is unchanged from `main`, and its timings and captured plans match `main`'s. Actual aux-hop statements captured from each relay binary with `auto_explain` (analyze + buffers), execution ms: | build | clean, 100 ids | clean, 188 ids | dirty, 100 ids | dirty, 188 ids | |---|---|---|---|---| | `main` (N-way `OR`) | 40 | 298 | 28785 | 62892 | | this PR (`MATERIALIZED` fence + `ANY`) | 23 | 45 | 1992 | 3539 | **What the fence changes** - On `main`, the planner combines `ORDER BY created_at … LIMIT` with an overestimated match count and walks each partition's primary key backward, expecting to stop early. With few real matches it scans the whole community: the dirty 188-id statement touches ~4.9M shared buffers on `main` vs ~0.6M with the fence. - The fence prevents that early-stop plan. Across the measured cases (1–188 ids, both pending states), the planner used the GIN tags index. The fence doesn't force GIN in general; a broad enough filter could still get a different plan. **Tradeoff:** the fence materializes every match before sorting. In this bench small limits stayed within run-to-run noise of `main`. A single hot id (5,000 reactions, limit 100) measured 30–34 ms unfenced vs 35 ms fenced. **Deadline:** each e-tag read (SELECT and COUNT) runs under a transaction-local `statement_timeout` of 20 s. It only tightens: a shorter operator timeout (`BUZZ_DB_STATEMENT_TIMEOUT_MS`) is kept. It's a per-statement limit, not a per-request one, so a request that runs several pages or hops can take longer than 20 s in total. The benchmark does not force the deadline. --------- Signed-off-by: Will Pfleger <[email protected]> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> * test(desktop): wait for channel head refresh before paging thread summary test (#7955) 🤖 ## Summary Main's Desktop Smoke E2E (4) job fails on `scroll-history.spec.ts` ("thread summary badge survives a retained older-history prepend"). This change fixes the test's timing. It does not change app code. When a channel opens, the app starts a live subscription. When that subscription is ready, the app fetches the newest page again, and that fetch replaces the whole loaded history. This test scrolled up before that refresh happened. On slower CI runners, the refresh came after the older page loaded but before it showed on screen. The older rows went away, the reader stayed at the top, and no more pages loaded. All three attempts failed that way. The test now calls `waitForMockChannelHeadReady` before it scrolls, like the other pagination tests in this file already do. That helper waits for the live subscription and its refresh to finish. ### Related issue Main CI failure at `15d44dcc` (https://github.com/block/buzz/actions/runs/36470599751/job/109092076235). The same failure was reported on #7827. ### Testing - Added temporary logs to see the order of events. Locally, the refresh ran after the older page had loaded: the loaded history went from 2 pages back to 1. The test passed only because it checked before the next render. With the wait, the refresh runs first and nothing replaces the loaded pages later. - `playwright test --project=smoke tests/e2e/scroll-history.spec.ts`: 18 passed. This test with `--repeat-each=8`: 8 passed. ### Follow-up (not in this PR) The app has the same race for real users. If someone scrolls up before the live subscription is ready, or during a reconnect, the refresh can drop the older history they loaded. If they are at the top of the list, paging stops until they scroll down and back up. Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> * fix(mobile): stale community selection during mobile invite setup (#7951) Buzz sets up starter channels when a person accepts an invite. If the phone already had a saved community, the first attempt could fail. Retry could then succeed. A delayed read could replace the new active community with the old one. The read now leaves the saved selection unchanged. The fix also delays channel-status updates until the channel provider finishes initialization. ### Before and after Controlled Flutter app captures against local relays, with the same delayed storage read. The before capture restores the pre-fix active-community provider in the test harness. The after capture uses the fixed provider. These are separate from the native iOS validation below. | Before: first attempt fails | After: first attempt succeeds | | --- | --- | |  |  | ### Validation - Regression tests reproduce the delayed-read ordering and passing timing control; removing the channel sequencing safeguards makes their regressions fail. - Production app against local relays passes the forced race on the first attempt. - Native iOS simulator with an existing saved community accepted the original invite using a fresh identity, joined on the first attempt, and opened the welcome channel without Retry. Signed-off-by: Tom Brow <[email protected]> * Schedule the deletion drain safely (#7827) ## Why The deletion engine already exposes a one-shot `buzz-admin deletions drain` command, but operators have no safe way to schedule it. A generic workload wrapper could expose arbitrary commands or inherit relay credentials, which would add a second control plane and widen the blast radius. ## What This PR adds an opt-in Kubernetes CronJob for the typed deletion drain command. It is stacked on #7818, which adds owner-request admission. ## How The chart keeps the job disabled by default. The CronJob uses `Forbid`, zero Kubernetes retries, bounded runtime and history, and only the database, Redis, and object-store settings required by the executor. PostgreSQL leases, checkpoints, and retry timing remain the sole durable retry authority. The closed `_operator-jobs.tpl` helper centralizes safe CronJob mechanics without creating an arbitrary command or environment registry. This leaves the touched system simpler than a general job framework would. ## Risk The deployment blast radius is low because the job is disabled by default. When enabled, it can run destructive deletion work that the database has already approved, so the chart limits concurrency, retries, credentials, and execution time. ## Testing Rendered every `deploy/charts/buzz/ci/*.yaml` fixture with `helm template`; all passed at `e68cb753bb6673aa1baa60046969bdb2d4738b62`. Ran the full independent Desktop, Tauri, and web lanes; all passed at `e68cb753bb6673aa1baa60046969bdb2d4738b62`. Flutter package resolution made no progress within three minutes, so the mobile lane is not claimed green. ## Bigger picture A later stack layer must inventory and approve admitted owner requests. Until then, this scheduler cannot make a new owner request destructive. Generated with Codex --------- Signed-off-by: tornquist <[email protected]> Signed-off-by: Codex <[email protected]> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Codex <[email protected]> Co-authored-by: Claude Opus 5 <[email protected]> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> * fix(desktop): resolve unlisted project channel requests (#7619) ## Summary - resolve project channel-creation approvals through the authoritative home-channel selector - permit owner-visible unlisted project homes when their channel and repository binding match - add regression coverage for the unlisted authoritative-home path ## Validation - `just ci` - focused project-home regression tests (10/10) - full Desktop suite (6,451/6,451) - `pnpm check` - `pnpm build` ## Manual verification After merging into a Desktop build, approve the pending LBX Copywriting workroom request from the Litbox OS project. Signed-off-by: crgallego <[email protected]> Co-authored-by: crgallego <[email protected]> * feat(relay): implement NIP-AR channel artifacts (#7919) ## Summary Implements NIP-AR channel artifacts (spec merged in #7791) on the relay, after a simplicity pass over the earlier two-agent implementation. An artifact is an editable record (kind 45010) with a stable identity (`d`), one home channel (`h`), and full-snapshot revisions chained by `prev`: - **Writes:** one transaction per write. It locks the artifact's head row, requires `prev` to name the current revision, records the revision in a ledger that survives retention, and advances the head. Two competing edits can't both win. A stale edit gets `conflict: artifact head changed`. Resubmitting an accepted event succeeds without applying it twice. - **Permissions:** writes pass the same gates as a kind 9 message in `h`. A move also passes them in the source channel. The move stores the destination snapshot and a relay-signed kind 45011 removal marker for the source in the same transaction, so both channels recover by replay. The marker never names the destination. - **Moderation:** redaction is the ordinary kind 9005 removal. The ledger keeps the ID so `prev` checks still work. Redacting the current revision retires the artifact. - **Reads:** HTTP `/query` and `/count` accept explicit `artifact: current|history` filters with exact multi-character tag matching (e.g. `#assignee`). Generic REQ and search return only current revisions. Kind 5 against artifacts is rejected. NIP-11 advertises the limits. - **Spec:** NIP-AR updated to match: kind 9 permission checks, 9005 redaction and retirement, no creator-only delete rule. A separate small commit routes `insert_channel_head_checked` through the metered writer pool (`acquire_writer`), like every other event write in that module. ### Related issue Follows #7791. No separate issue found. ### Testing - Clippy clean. - Unit lane passes. Failures seen along the way came from agent env leaking into `buzz-acp` tests (they pass in a clean env) and known timing flakes (`keepalive_resets_idle_past_deadline`, two `buzz-agent` databricks tests that passed on retry). This diff doesn't touch those crates. - Postgres lane passes, including new `artifact_postgres_tests` in `buzz-db` and `buzz-relay`. Three presence tests need `REDIS_URL` set. - Docker smoke against a fresh local relay, 29/29: create and idempotent resubmit, update and stale conflict, current, `#assignee` and history queries, generic REQ, kind 5 rejection, delete and restore, move with source marker, private-channel isolation, and 9005 retirement. - A pasteable curl walkthrough of the same flows was shared with the reviewer. Draft until the human hands-on check (AGENTS.md step 3) is confirmed. The pre-push hook was skipped because every lane above had already run on this tree. CI is the gate. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz> Signed-off-by: murderbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz> Co-authored-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> Co-authored-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz> Co-authored-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz> Co-authored-by: murderbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz> * Automate owner deletion preparation (#7830) ## Why Owner deletion admission stops at `submitted`. Manual inventory and approval still stand between owner intent and the existing deletion executor, preventing self-serve deletion. ## What The existing `buzz-admin deletions drain` process prepares and automatically approves operator-attested owner requests. Operator-origin requests retain manual approval. ## How The drain prioritizes approved work, then claims one owner submission with the existing generation lease. It freezes inventory and records digest-bound `owner_automatic` approval in one transaction before entering the unchanged executor. Before approval, the transaction rechecks archived state and current ownership. It locks the deletion advisory fence, community, owner memberships, and request in that order. A regression test pins privileged abort during a live preparation lease. What got simpler: one drain still owns preparation and execution. There is no second worker, queue, lifecycle, or retry authority. The rebased branch contains only this feature and its review fixes. ## Risk Automatic approval permits irreversible deletion without a human grace period. Operator attestation is not a cryptographic owner signature. Provenance, locked authority checks, generation fences, and digest-bound approval constrain the operation. This remains work in progress. Broad test failures and incomplete mobile checks are disclosed below; publication is not a merge-ready verdict. ## Testing Published head: `e059ae9056b447ca3b5d0c2893da86550ef16f5c`. All seven commit trees match tested source `5605f27eaece1e809f746cc1839b55c7ceef8d80`; only attribution metadata changed. Source-SHA results below are not new-head execution receipts. At source commit `5605f27eaece1e809f746cc1839b55c7ceef8d80`, isolated PostgreSQL tests showed stale archive and ownership fixtures fail closed after the fix. Before the fix both reached automatic approval. Removing abort's generation increment or stage transition made the new live-lease regression fail; restored production passed. `just test` failed in two ACP native-git fixtures and one agent body-timeout test. The failing source files match the pinned base, but there is no exact-base execution comparison proving those failures unrelated. `just test-unit` also failed in the ACP fixtures, leaving 18 tests unrun. `just ci` stopped in Flutter tooling package resolution; separately run non-mobile lanes do not constitute a full `just ci` pass. No live tenant deletion or human testing was performed. A dedicated populated `0052` → `0053` approval-backfill fixture remains a coverage gap. Final-head hosted CI and security review are required. ## Bigger picture Rebased directly onto main `12670bd0f037c66a682272bb81c46c3f254fad74`; prerequisite #7818/#7827 are already merged. Main owns `0052_channel_artifacts`, so this feature's automatic-approval migration is now `0053`. Client/KGoose and quota work are separate PRs and are not part of this diff. Generated with Codex --------- Signed-off-by: Codex <[email protected]> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Codex <[email protected]> Co-authored-by: Claude Opus 5 <[email protected]> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> * feat(mobile): add the contextual identity-name resolver (#7894) 🤖 ## Summary When two identities share a display name, Buzz mobile shows the same label for both. A reader cannot tell a person from an agent, or one person's agent from another's. For example, a channel can show three agents and one human all named "murderbot". This PR adds the naming rules that fix this, without changing any screen yet. It ports the contextual identity-name contract (v1) from the desktop app to Dart. Given the identities shown together, the resolver gives each public key a distinct label: - A human keeps the plain name. Agents that share it get a qualifier. - The viewer's own agents are named before other people's agents. - An agent is qualified by its owner's name first ("Wes's Honey"). A short public-key suffix is added only if that is still ambiguous ("baxen's murderbot · xa8v"). It also adds two providers that screens will use: - `channelIdentityNamesProvider` compares names against a channel's members, plus any referenced non-member such as an old author or an outside mention. - `watchIdentityNames` compares a set of identities shown together outside a channel, and loads their profiles. The next PRs in this stack connect these to the screens. Part 1 of 3: #7894 (resolver) → #7895 (conversations) → #7896 (lists, Search, Pulse). ### Related issue Related to #2910 (default "Fizz" agents from different users collide in one channel). ### Testing - The contract's 31 portable fixture cases run against the production resolver. The fixture file is copied unchanged from the contract, so reviewers can skip it. - Unit tests cover owner and suffix qualification, the viewer's own identities, and invalid public keys. - Provider tests check that a channel keeps its names while its roster reloads, that an offline cached bot still counts as an agent, and that changing relay or account never shows the old roster. - `flutter analyze` is clean. No UI change, so there are no screenshots. --------- Signed-off-by: Logan Johnson <[email protected]> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> * docs: specify desktop-driven mobile push suppression (#7809) ## Summary Define when desktop activity suppresses otherwise eligible mobile message notifications: a ten-minute inactivity window, short-lived relay-owned suppression, and per-installation push preferences. Include a shared glossary and renewal timing diagram. The gateway remains unaware of suppression. This is a design draft ready for review, not an implementation-ready protocol. Wire format, synchronization, and several failure-handling details remain explicitly open. Please also review the tension between the agreed relay-confirmed preference changes and VISION_MOBILE.md's requirement that stopping notifications must not depend on a relay response. ### Related issue Related: #3234 covers per-channel notification settings and push filtering; this spec focuses on desktop-activity suppression. No duplicate spec PR found. ### Testing Documentation-only change. Local Markdown links and anchors resolve; whitespace validation and commit hooks passed. The full local CI gate could not complete: dependency compilation exhausted local disk space (OS error 28). GitHub checks currently report no failures. --------- Signed-off-by: Tom Brow <[email protected]> Co-authored-by: Diem Nguyen <[email protected]> * docs(nips): fix stray angle brackets in created_at clauses (#4486) ## Summary The `<` in a `created_at<` condition is the comparison operator, but in three places it had been paired with a closing `>` as if it opened a `<placeholder>` bracket: - **NIP-OA** clause list: `created_at<unix-timestamp>` → `created_at<unix-timestamp` - **NIP-OA** satisfaction rule: `created_at<t>` → `created_at<t` - **NIP-GS** "Conditions in Git Context": `created_at<t>` → `created_at<t` (line 437 of the same file already used the correct form) This matches the `created_at>...` forms used alongside them, which never carried a closing bracket. Also normalizes the NIP-OA headings to ATX style and pretty-prints the signed-event JSON example for readability. ## Verification Docs-only change. The NIP-OA test vectors are untouched and were re-verified by recomputing them: the preimage SHA-256, `tag-bytes-hex`, and the signed-event `id` all still match the documented values. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: zmeyer44 <[email protected]> Co-authored-by: Claude Fable 5 <[email protected]> * feat(web): add Browse releases link next to invite download (#2255) Related to https://github.com/block/buzz/issues/2093 Invite pages offered a platform-resolved binary download with no path to the GitHub releases listing for users who wanted information first. ### Changes - Keep the smart **Download it now** CTA - Add an always-visible **Browse releases** link pointing at `https://github.com/block/buzz/releases` - Extend invite smoke coverage for the new link Note: the marketing-site “Get the App” button on buzz.xyz appears to live outside this repository; this improves the in-repo web invite flow. Signed-off-by: Ian Alloway <[email protected]> Signed-off-by: Fizz <300bd4cedcdddb62f77935de51c301034bdb4cd97da9aaca46d59673a1ebc7cf@buzz.block.builderlab.xyz> * feat(buzz-relay): NIP-FI stateless enforcement (S3) — upgrade gate, NIP-42 pairing, session lifetime, JWKS warm (#7224) ## What this PR does Implements NIP-FI stateless enforcement at the relay WebSocket upgrade point. Every connection attempt on a NIP-FI–enabled relay must carry a valid federated identity assertion signed for the connecting Nostr pubkey; the relay validates it before the first frame is read, pairs it with NIP-42 AUTH, and enforces a bounded session lifetime without persistent state. ### Core enforcement seam - **Assertion at upgrade** (`nip_fi_upgrade.rs`): identity header is parsed with the same `extract_bearer_token` and denied with the same `http_denial` as HTTP ingress (`nip_fi_http.rs`), JWT verified (RS256/ES256/EdDSA), expiry checked, and the Nostr pubkey embedded in the sub claim must match the NIP-42 AUTH pubkey that arrives in the socket within the AUTH timeout. Denial returns exact byte contracts — `401 + WWW-Authenticate: Nostr + "authentication required\n"` for missing evidence, `403 + "evidence rejected\n"` for malformed or invalid evidence. The active HTTP/1.1 gate runs before `bind_community`, so denied upgrades pay zero DB cost. - **NIP-42 pairing** (`nip_fi_gate.rs`, `audio/handler.rs`): after the assertion is validated at upgrade, a `SessionAdmissionGate` is created with the assertion's expiry deadline and a `CancellationToken`. The gate is passed to every effect-acquiring path so no new effect permit is granted after the session expires. AUTH is verified against the assertion pubkey; a mismatched AUTH key receives `NOTICE restricted: authorization denied` and a socket close. Every other post-upgrade AUTH denial on an FI connection uses the same terminal shape — the canonical spec NOTICE on the terminal queue, then close — so frame type and close behaviour reveal nothing about private policy: ban, allowlist, or relay-membership denial → `restricted: authorization denied`; ban/allowlist/membership lookup failure or an internal verifier failure → `restricted: authorization unavailable`; an invalid NIP-42 proof → `restricted: evidence rejected`. Connections without an assertion keep today's `OK false` replies. - **Session lifetime** (`nip_fi_session.rs`): three independent deadlines bound each session — token `exp`, max assertion age from config, and relay max connection lifetime. The first to fire cancels the gate and sends the terminal denied frame. - **Effect admission at expiry** (`audio/join.rs`, `handlers/req.rs`): no new effect permit is granted after expiry. Two paths carry calculated budgets. The audio **lease resolver** holds its permit for at most `worst_case::PERMIT_HOLD` (14.5 s), and cancellation **during that resolver** reaches socket close within `worst_case::EXIT` (25 s: resolver, release of the won lease, terminal flush). Redis checkout has one total deadline and directory scripts are single-round-trip `EVAL`, which is what makes those budgets hold. When post-write fence verification fails after a landed CAS, the acquire keeps the reply and attempts an exact owner+generation cleanup on the same connection without a serving-write lease; if that cleanup fails or its reply is uncertain, the lease expires by its 30 s TTL. Neither number bounds audio close in general: other admission and teardown waits (for example the channel lookup after the resolver) and the known limitations below have no client-side deadline. REQ liveness, search and history reads race gate cancellation: a lost race drops the read, releases the permit, retires the claim through `close_if_owner`, and sends no further EVENT or EOSE. A history statement timeout ends the read with a timed-out outcome, and its owner-checked retirement (`CLOSED`, map, index and topic release) runs after the race with the permit dropped, so cancellation cannot drop it halfway. Subscription registration stays one uncancellable unit, and the permit is still taken before the claim. - **JWKS recovery** (`main.rs`): builds on main's `warm_nip_fi_jwks_snapshots` / `nip_fi_jwks_refresh_loop`. Issuers warm concurrently and the first refresh is seeded from the warm result (one interval for a warm issuer, 5 s for a cold one). A refresh that finds no live snapshot retries at the fast cadence (5 s after a warm issuer dies, doubling to a 300 s cap) instead of waiting a full interval, and `run_jwks_refresh_supervisor` restarts the loop after a panic (1 s → 60 s backoff); On shutdown it is cancelled and awaited before the drain backstop is disarmed; cancel drops an in-flight fetch and starts no further issuer, so the join stays inside the drain bound. Logs carry `issuer_index` only. ### Known limitations - **Known limitations: bounded only by the dependency.** No new effect is admitted after expiry. Two awaits that are already admitted have no client-side bound, so if a dependency stalls at expiry, teardown waits for it: - **Audio join commit:** its effect permit spans `tx.commit()`, room publication and the `event_mentions` insert. During a DB or Redis stall, audio cleanup and socket close wait for the dependency. - **REQ subscription setup:** the first subscriber to a topic sends `Subscribe` to the pubsub task through a bounded channel (capacity 4096). The send blocks only if that queue is full, which happens when the pubsub task isn't keeping up (for example during reconnect backoff or a burst). Registration holds the connection's `conn.subscriptions` lifecycle lock across the send, so a blocked send also blocks other claims on that connection, including search, and any cleanup waiting on that lock. The send itself predates this PR; holding the REQ permit across it is new here. ### Archive/join serialization `commit_participant_join` now takes a row-level write lock (`SELECT ... FOR NO KEY UPDATE`) on the channel row at step 3. This serializes all join commits against concurrent `archive_channel` calls — the archive UPDATE (`archived_at` non-key column) blocks on the row lock, closing the READ COMMITTED race on both the `Existing` and `AutoAddRequired` paths. ### HTTP/2 extended-CONNECT gate (latent path) The root WebSocket handler uses a belt-and-suspenders approach. The HTTP/1.1 path (currently the only live WebSocket shape — workspace Axum does not enable `http2`) is gat…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses point 3 of previous comment "3. Fail fast on a bad BUZZ_OPERATOR_LISTENERS (config.rs:857)"
Summary
BUZZ_OPERATOR_LISTENERSvalues fromConfig::from_envasConfigError, including invalid UTF-8.Config::from_env.Context
Follow-up to the review of #7793 specifically reverting some config logging parts. Invalid listener configuration currently logs an error and silently disables listener delivery.
Verification
cargo fmt --all -- --checkpassed with Hermit.