Files
buzz/mobile/ios/BuzzPushKit/Sources
Tom Brow 97ede701c0 fix(mobile): send hosted relay assertion from native media and tombstone paths (#9069)
Follow-up to https://github.com/block/buzz/pull/9056, from an audit for
other places that miss the hosted relay assertion. Hosted and
organization relays require `Nostr-Federated-Identity` on every
non-exempt HTTP request and WebSocket upgrade. The Dart
`HostedRelayHttpClient` adds it, but anything that downloads or connects
outside that client has to add it itself. The notification service
extension and the iOS emoji picker both missed it, and these places do
too:

- **Voice notes** (`just_audio`, mainly Android) and **video preview
first frames** fetch media natively with only the Blossom
`Authorization` header. Both now use `nativeMediaHeadersProvider` from
https://github.com/block/buzz/pull/9056.
- **Push-lease tombstones**: removing a community, and the durable
revocation outbox, open a one-off relay socket with no headers. A hosted
relay refuses the upgrade, so the lease is never revoked and the outbox
keeps retrying. Both now ask `HostedRelayAuth` for that community's own
assertion, because the community being removed may not be the active
session.
- **Notification avatars** (iOS notification service extension):
relay-hosted sender avatars were fetched with Blossom auth only. The
loader now takes the same `BuzzRelayAssertionIssuer` the resolver uses
for `/query`. Signed requests already refuse redirects.

The iOS Me-chat agent picker also fetched avatars with no headers, but
https://github.com/block/buzz/pull/9079 removed that native screen, so
this PR no longer touches it.

### Validation

- Added tests, each checked to fail when the code it guards is reverted:
- A queued push-lease revocation and a community-removal tombstone both
send `Nostr-Federated-Identity` on the WebSocket upgrade, as background
requests. Runs against a local relay socket that handles NIP-42 auth.
- A notification avatar on an organization relay is downloaded with the
issued assertion (BuzzPushKit).
- Issuing for a second community keeps the first one's cached assertion.
A background request records no failure, never backs up the key, and its
failure never reaches the active community's reconnect waiting on it.
- Background issuance never binds an unbound account to its key,
bypasses the shared assertion cache (each attempt gets a fresh
assertion, and foreground use never inherits one that skipped the key
backup), and its failure is never handed to a foreground caller waiting
on it. When it can't get an assertion it sends none, so a non-enforcing
relay still accepts the tombstone.
- A notification avatar whose organization session is replaced
mid-download is refused and dropped from the cache (BuzzPushKit).
- Voice notes and posterless video previews renew an expired assertion
before the native fetch, and fall back to cached headers if renewal
fails. A voice-note attachment journey binds the production provider.
- Cached reads never present a signed-out or replaced account's
assertion.
- `flutter analyze` is clean. `swift test` passes for the BuzzPushKit
avatar loader tests, and `flutter build ios --simulator --debug`
succeeds.
- Full mobile suite (`just mobile-check` and `just mobile-test`) passes
at 2c84940832.
- Automated full-app run on an iOS simulator (debug build of this PR
before it was rebuilt on main, signed in to the internal Builderlab
community with two more communities saved). Passed: launch and channel
list, image attachments and avatars in a channel, New chat sheet kept
open for about 80 seconds before the agent list was opened (all agent
avatars loaded, so headers are requested per download), dismiss and
reopen, background and resume, switching to a second community (its icon
and avatars loaded), and removing a community. The native New chat sheet
is unreachable in the normal flow on this branch (a new chat opens the
Flutter draft page; the native sheet only opens when resuming an
unfinished chat), so that check ran with a temporary local bypass that
was reverted. Limits: none of these communities requires the hosted
relay assertion in this build, so the run shows the Swift-to-Dart header
bridge, the per-relay cache and the tombstone path cause no regression,
not that the assertion is sent or accepted. No voice note, video preview
or deleted message was reachable. Screenshots are omitted because they
show internal channel content.
- Limitations: no end-to-end run against a hosted relay. Simulator
sign-in to a hosted relay needs SSO, so a hosted-relay run needs a
device build.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
2026-11-05 12:32:18 -08:00
..