Commit Graph

11 Commits

Author SHA1 Message Date
Tom Brow
4af2324d85 fix(mobile): keep relay sessions stable during push lease updates (#7745)
Enabling push notifications could repeatedly reset the relay session:
reserving a lease generation updates community metadata, which rebuilt
the relay configuration even though the connection identity was
unchanged. This made the avatar and inbox flash and prevented the new
lease from publishing.

Select only the community ID, relay URL, and signing identity as
configuration dependencies. Push bookkeeping keeps the session intact;
real community, URL, and identity changes still notify listeners.

### Validation
The production-provider regression test failed before the fix and passes
afterward; 48 focused mobile tests and static analysis pass.

A personal signed profile build on a physical iPhone published a fresh
lease and received a background notification with the actual message
text. The user confirmed the flashing stopped and tapping the
notification opened the correct community/channel and highlighted the
exact message.

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-09-18 12:10:26 -07:00
Tom Brow
76f7e5ef0c feat(push): make gateway origin configurable (#7261)
Make the push gateway origin configurable across the relay, gateway,
mobile client, and deployment chart.

This is a fresh deployment. The gateway has only run on personal
development infrastructure, and push has no existing enabled users.
Legacy migration, configured-to-unconfigured mobile transitions, and
rolling upgrades from pre-launch gateway binaries are not supported.

Populated pre-launch databases are rejected with an informative error
before migrations run; their data is untouched. Mobile builds may omit
the gateway URL, leaving push unavailable. Once push is enabled,
subsequent builds must retain that configuration.

### Validation

- All applicable CI checks pass at
`5f546cfaf403f0386f146d213adfb4392f4ec29c`, including Rust,
mobile/Swift, desktop/integration, security, image builds, and Helm
checks. An unrelated agent cancellation test passed on retry.
- PostgreSQL CI passed 384 tests, including the production-entry-point
regression for refusing populated pre-launch databases before
migrations, fresh initialization, and subsequent initialized
deployments.
- Focused local validation passed all 10 gateway PostgreSQL tests under
the CI discovery filter and all 3 unconfigured-mobile tests.
Repository-wide PostgreSQL discovery validation and the complete
pre-push hooks passed.
- Physical-device APNs/App Attest behavior and deployment to a live
cluster were not verified in this work.

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
2026-09-15 14:56:45 -07:00
Tom Brow
c432a111ca feat(mobile): push notifications MVP (#6269)
This PR implements MVP, iOS-only,
[NIP-PL](8d2d0ff5ad/docs/nips/NIP-PL.md)-compliant
push notifications.

A relay with `BUZZ_PUSH_ENABLED` will send a push notification for any
message that appears in the in-app Notifications tab.

## Enrollment flow
The first time the client first connects to a relay with
`BUZZ_PUSH_ENABLED`:
```mermaid
sequenceDiagram
    autonumber
    participant App as Buzz iOS app
    participant iOS
    participant Relay as Buzz relay
    participant Attest as Apple App Attest
    participant Gateway as Push gateway

    App->>Relay: Fetch NIP-11 push capability
    Relay-->>App: Push profile, current relay public key, and limits

    par
        App->>iOS: Request notification permission
        iOS-->>App: Permission result
    and
        App->>iOS: Register for remote notifications
        iOS-->>App: Device token
    end

    App->>Gateway: Request installation challenge
    Gateway-->>App: Single-use challenge
    App->>Attest: Attest installation transcript
    Attest-->>App: Attestation proof
    App->>Gateway: Enroll device token and proof
    Gateway-->>App: Installation handle

    App->>Gateway: Request delegation challenge
    Gateway-->>App: Single-use challenge
    App->>Attest: Assert relay-key delegation
    Attest-->>App: Assertion
    App->>Gateway: Create delegation
    Gateway-->>App: Opaque endpoint grant

    App->>Relay: Publish encrypted push lease and filters
    Relay-->>App: Lease acknowledged
```

## Push-time flow

When a notification-eligible event is received by the relay:

```mermaid
%%{init: {
  "sequence": {
    "actorMargin": 20,
    "width": 110,
    "messageMargin": 18,
    "diagramMarginX": 8,
    "wrap": true
  }
}}%%
sequenceDiagram
    autonumber
    participant Relay as Buzz relay
    participant Gateway as Push gateway
    participant APNs as Apple Push<br/>Notification service
    participant iOS
    participant NSE as Notification service<br/>extension

    Relay->>Gateway: POST /v1/deliveries/apns<br/>opaque endpoint grant, request ID, expiry, NIP-98 authorization

    Gateway->>APNs: POST /3/device/{device-token}<br/>topic, request ID, expiry, constant mutable-content payload
    APNs-->>Gateway: 200 OK: request accepted
    Gateway-->>Relay: 200 OK: accepted status

    APNs-->>iOS: Notification: constant reconnect alert<br/>mutable-content = 1
    iOS->>NSE: Invoke extension<br/>original notification content

    NSE->>Relay: POST /query: subscription filters, limit 10<br/>NIP-98 authorization
    Relay-->>NSE: 200 OK: signed Nostr events<br/>kinds 9, 40002, 45001, or 45003

    NSE->>iOS: Complete notification: title, body, subtitle<br/>thread ID, exact-message target
```

relay → push gateway → APNs -> NSE -> Notification Center

## Known limitations

The APNs wake payload is intentionally constant and opaque: it contains
no originating community or message identifier, in keeping with the
implemented NIP-PL privacy design.

The Notification Service Extension must therefore reconnect to the relay
and resolve eligible messages after each wake. Around overlapping wakes,
timing boundaries, or resolution windows, notification presentation may
occasionally omit an expected message or display a message more than
once.

This best-effort behavior is deliberately accepted for the current
implementation and will be measured during the internal rollout to
determine whether the user experience is acceptable before any broader
deployment; the implementation does not claim exactly-once presentation.

## Validation

Live end-to-end hardware validation used an internal remotely hosted
development relay and push gateway, the APNs sandbox, and a physical
iPhone 12 mini:

- A second real Buzz client published a uniquely marked message through
the hosted relay.
- The relay matched the message and sent the constant opaque wake
through the hosted gateway. The gateway made an actual APNs request; no
`simctl push` or simulated notification was used.
- The iPhone received the notification on its lock screen. The
Notification Service Extension reconnected to the relay, fetched the
event, verified its ID and signature, and replaced the placeholder
content with the real notification title and body.
- After the app populated its shared presentation cache, a final marked
notification visibly showed the sender display name, sender avatar, and
hashtag-prefixed channel name.
- Tapping a lock-screen notification opened Buzz and exercised the
notification-response path and navigated to the corresponding message.

Final validation with a dogfood-signed artifact and production App
Attest/APNs configuration remains a release step.

## Independent pre-reviews

- **First pass:**
[Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=74ab9a93bb227f3e762568f1cf9fee66d7495b0edc3918735ff787238b9cc585)
found missing transient retries, executor-key rotation suppression,
duplicate installation renewal, and an unauthenticated challenge write
amplifier. These were resolved by [retry-safe
bootstrap](https://github.com/block/buzz/pull/6269/changes/12c66ea62)
and [authenticated renewal plus a cross-replica
quota](https://github.com/block/buzz/pull/6269/changes/8e5ece0bd).
[sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=d26d53daa4684669e2ed354638241f13f36c3a97027fe8b4dd738aff09038962)
found delegation generation burning and an edited applied migration,
resolved by [exact-generation
revocation](https://github.com/block/buzz/pull/6269/changes/c26d2159d)
and a [forward-only
migration](https://github.com/block/buzz/pull/6269/changes/956c1d099).
[k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=d43139138a0b15f806cbdbeeedd8f69d992cadf2e805876db6fdde6a34c7eda1)
found no blockers.
- **Exact-head re-review:**
[Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=a897721673459301b0cf26e8b85a1478d7ebbb56a4621f93d774c98d395b8f68),
[sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=fb2159f709ec68f74f7b21459acd76da0e8a7c5c0f3d469f99826b0cc2380849),
and
[k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=2ce2842910435f562e9d9cc718595848f281b122c94605e523a4b964254b8bfb)
independently returned **NO BLOCKERS** at `7eb3a650b`; k3-max also
revalidated every remediation and the endpoint-specific App Attest
enrollment bound.

---------

Signed-off-by: Tom Brow <tomb@squareup.com>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Jordan Mecom <jm@squareup.com>
2026-08-28 15:15:09 -06:00
Krishna C
047533c56c fix(mobile): keep TLS on relays joined by invite (#3139)
## Summary

Communities joined via an invite link never connect: the app dials
`ws://` on port 80 instead of `wss://` on 443 and sits on
"Reconnecting…" indefinitely.

`RelayConfig.baseUrl` is documented as an HTTP origin, but the two
onboarding flows disagree on what they persist:

- **Device pairing** validates and stores `https://` —
`pairing_provider.dart:657` throws on anything else.
- **Invite join** stores the relay URL straight off the invite link, and
`deep_link.dart:165` always emits `ws://` or `wss://`.

`wsUrl` only special-cased `https://`, so a `wss://` base fell through
to the plaintext branch:

```dart
final scheme = uri.scheme == 'https' ? 'wss' : 'ws';   // 'wss' is not 'https'
```

The claim request itself succeeds, because `_claimUrlFromRelay`
(`invite_join_provider.dart:242`) maps `wss → https` explicitly. Only
the socket path is missing that conversion — which is why the community
appears, correctly named, and then never loads.

The same `baseUrl` also feeds `/query` (`relay_session.dart:136`), media
upload (`media_upload.dart:765`), Blossom auth (`media_auth.dart:128`)
and `relayClientProvider` (`relay_provider.dart:113`), so those requests
were malformed too. Where port 80 *does* answer, it is additionally a
silent TLS downgrade after `validateInviteRelayUri` insisted on
`wss://`.

This folds the websocket schemes back to their HTTP equivalents in
`baseUrl` itself, so every consumer is correct by construction rather
than needing a second getter remembered at each call site, and
communities **already persisted** with `wss://` are repaired on read
without a migration. `community_icon_provider.dart:46` already performs
this same conversion locally.

One subtlety worth flagging for review: the normalization is derived in
the getter rather than applied in the constructor, so the constructor
stays `const`. The compile-time fallback at `relay_provider.dart:77`
relies on const canonicalization for a stable identity across rebuilds,
and Riverpod's `defaultUpdateShouldNotify` is `previous != next`
(`element.dart:361`), which falls back to identity for this class. A
`factory` constructor here yields a fresh instance per rebuild, which
tears down and resubscribes every listener —
`channels_provider_test.dart` catches it as an unexpected unsubscribe
during reconnect.

### Related issue

Fixes #2662.

### Testing

`flutter test` — **705 passed, 1 skipped, 0 failed**
`flutter analyze` — No issues found
`dart format --set-exit-if-changed .` — 249 files, 0 changed

Run against the Hermit-pinned SDK (Flutter 3.41.7 / Dart 3.11.5),
matching CI.

10 new unit tests in `mobile/test/shared/relay/relay_config_test.dart`
covering both onboarding schemes, `http`/`https` passthrough,
non-default ports, and agreement between the invite and pairing paths
for the same relay.

Verified end-to-end against a self-hosted relay behind `tailscale
serve`, which terminates TLS on 443 and leaves port 80 closed. Relay
logs show the invite claim succeeding over HTTPS at the moment of
joining, while no WebSocket connection ever arrives — no `WebSocket
connection established`, no NIP-42 auth, no `kind:0` profile, no push
registration — across the relay's entire history, even though the member
row is present and correct. Port-80 refusals are not logged by
`tailscaled`'s netstack, which is why the retries leave no trace
server-side. Reproduced on both iOS and Android.

---------

Signed-off-by: Krishna C <github@kumb.uk>
2026-07-29 12:02:42 -07:00
Wes
3e76481a14 refactor(clients): standardize product naming on community (#1858)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
2026-07-14 11:11:02 -07:00
Wes
c63e018b05 Rename mobile app to Buzz (#955)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
2026-06-10 13:38:11 -07:00
renovate[bot]
fbbb2d1d6e fix(deps): update dependency pointycastle to v4 (#687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Will Pfleger <wpfleger@block.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
2026-05-26 22:16:00 +00:00
tlongwell-block
454781caae Sprout speaks Nostr. Nothing else. (#475) 2026-05-05 12:03:06 -04:00
Wes
57186963e3 feat(mobile): multi-workspace support (#408)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-27 13:47:06 -07:00
Wes
94d4e16461 feat(mobile): websocket streaming with real-time messaging (#323)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:20:35 -07:00
Wes
08be36471e feat(mobile): add channel list with relay integration (#315)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 08:42:29 -07:00