mirror of
https://github.com/block/buzz.git
synced 2026-11-08 10:21:46 +00:00
7fe3e6f9c3fcc504fbd25b071fdcef3c5ab336d6
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
041956ea44 |
fix(ios): saturate the push dedupe cutoff so extreme timestamps can't crash (#9010)
## Context Follow-up to [#8973](https://github.com/block/buzz/pull/8973), from Codex's [review](https://github.com/block/buzz/pull/8973#discussion_r4458876285) of its final head. The push dedupe cutoff subtracts the 15 minute reorder window from the newest shown time. A signed event can carry any `created_at`. One within 900 seconds of `Int.min` made that subtraction overflow and trap, crashing the Notification Service Extension. The claim was never saved, so every later push crashed the same way while the relay kept returning that event. The relay's own drift check normally rejects such timestamps, but the extension shouldn't depend on that. ## Change The cutoff now uses saturating subtraction (`BuzzNotificationCache.presentationCutoff(after:)`), in both the claim check and retention pruning. ## Validation - New test `testExtremeTimestampsDoNotCrashTheClaim` claims messages dated `Int.min`, `Int.min + 1`, now, and `Int.max`. It checks that each behaves like any other message and nothing crashes. With the plain subtraction restored, it crashes with signal 5 (overflow trap). - Full BuzzPushKit suite: 108 XCTest and 66 Swift Testing tests passed. - Automated app test: not run. This is a two-line arithmetic change inside the extension's cache. The live APNs run on [#8973](https://github.com/block/buzz/pull/8973) covers the flow. 🤖 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: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7d40a66126 |
fix(ios): stop repeating an old message in push notifications (#8973)
## Context Pushes from the gateway carry no event reference (`crates/buzz-push-gateway/src/model.rs`), so the iOS Notification Service Extension resolves each push by querying the user's NIP-PL lease filters and showing the newest matching message. Any push whose trigger wasn't the newest message (a reaction, an edit or deletion, a community query that failed, or several pushes arriving for one message) showed that same newest message again. Tom saw one message notify four times over three hours. ## Behavior - The extension now remembers which messages it has shown, per community and account, in its existing notification cache (`BuzzNotificationCache`). - Each push shows the newest message not yet shown, across all communities. If every candidate was already shown, it shows the generic "You have a new message." text. - A message more than 15 minutes older than the newest one already shown, in any configured community (including one whose query failed), is treated as already covered. So a stale backlog item can't surface hours later, even from a community where nothing has been shown yet. 15 minutes matches the relay's `created_at` drift bound (`MAX_TIMESTAMP_DRIFT_SECS`), so a sender with a slow clock still gets through. A future-dated `created_at` is capped at the time it was shown, so it can't hold back later messages. - Selection and claim happen in one locked cache call. It reads each community's store once and writes only when it records a claim, so concurrent pushes can't claim against a stale view, and a rejected candidate never rewrites the store. - Retention drops only entries the age check would reject anyway. The rest are capped at the union of the newest 256 by capped time and the newest 256 by `created_at`. So neither a burst of messages nor future-dated entries can evict a message the relay still returns. The field is optional, so existing caches still decode with no version bump. - Only the newest 10 matches per community are fetched, as before. If more than 10 messages arrive before their pushes resolve, the extra pushes show the generic text. - The claim happens before presentation. If presentation then fails, that push shows the generic text and the message isn't offered again, which is better than repeating. - If the cache can't be read, dedupe is skipped (fail open), so new messages are never hidden. If a claim can't be written, the new message is still shown. An already-shown message is still rejected, since rejecting needs no write. A revoked scope is skipped. - Dedupe state lives in the extension's `Caches` directory, which iOS can purge. After a purge, the next push can show the newest message again, once. This is the NSE-only fix. It doesn't change the gateway payload or NIP-PL. ## Validation - New test `testEachPushShowsANewMessageAndNeverRepeatsOne` uses a fresh cache instance per push, to model separate extension processes. It covers: a repeat push giving the generic text; out-of-order resolution (late, then early, then nothing); a stale message outside the window; and the future-date clamp. I checked by mutation that removing any one of the three guards (id check, window, clamp) fails it. - New test `testShownMessagesStayShownAcrossCommunitiesFailuresAndBursts` covers the review findings. It checks that: - an old message isn't shown from a community with nothing shown yet, including when the other community's query fails; - a burst of 100 newer messages doesn't evict a shown mention the relay still returns; - a read-only store neither repeats a shown message nor hides a new one; - a future-dated message isn't shown again after later messages. - New test `testFutureDatedEntriesDoNotEvictAClaimedMessage`: 257 future-dated claims don't evict a just-claimed message. - Mutation checks all fail these tests: a candidate-only watermark, retention by `created_at` only, a 64-entry cap, and pruning by capped time. - Full BuzzPushKit suite on `15913d0`: 106 XCTest and 65 Swift Testing tests passed. - Live relay run (on `baac384`): a scratch test drove the real resolver and cache against a local relay. One key posted messages and another resolved five pushes (not committed): | Push | `main` | This branch | | --- | --- | --- | | 1 (after "Hello 1") | Hello 1 | Hello 1 | | 2 (no new message) | Hello 1 | You have a new message. | | 3 (after "Hello 2", "Hello 3") | Hello 3 | Hello 3 | | 4 (no new message) | Hello 3 | Hello 2 | | 5 (no new message) | Hello 3 | You have a new message. | - Automated app check: built and launched the branch on an iOS 27 simulator. The app started and showed the signed-in home screen. Only the extension uses the changed types. - An independent fresh-context review found no blockers. Its comment suggestions were applied. ### Limitations I couldn't run a real APNs push end to end. Dev-signed builds get no gateway pushes, and `simctl push` on iOS 27 doesn't invoke the Notification Service Extension. The live relay run covers the resolver and cache logic the extension runs. 🤖 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: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
73962435b6 |
Use upstream C secp256k1 for native Nostr signatures (#8398)
Replace the Swift secp256k1 wrapper with a small native adapter over Bitcoin Core libsecp256k1. This is an alternative to the wrapper upgrade in [#8381](https://github.com/block/buzz/pull/8381), not an additional change to merge alongside it. The adapter covers the three Nostr operations Buzz needs: x-only public-key derivation, BIP-340 signing, and signature verification. It compiles pinned, unmodified upstream C source through SwiftPM. There is no build plugin, plugin approval script, or underscored Swift import. The app and notification extension use an explicit C module map and header search path. This trades the third-party Swift wrapper for a Buzz-owned adapter and responsibility for updating the vendored C source. Upstream v0.8.0 source, its license, and update provenance are included. The release Xcode pin remains separate in [buzz-releases#110](https://github.com/squareup/buzz-releases/pull/110). ### Validation - Native package tests passed on Xcode 27.0 and 26.5: 96 XCTest tests and 65 Swift Testing tests per toolchain. - Added tests cover the official BIP-340 vectors for 32-byte Nostr messages, invalid secret keys, and malformed input lengths. - Full unsigned Release app and notification extension build passed on exact Xcode 27.0. - Vendored files were compared byte-for-byte with upstream v0.8.0. - Full local `just ci` passed. - [Hosted full unsigned iOS Release build](https://github.com/block/buzz/actions/runs/38924020583/job/117051237094) passed. [Hosted Native Swift validation](https://github.com/block/buzz/actions/runs/38924020583/job/117051237153) passed, including the complete simulator build and native controls/push navigation tests. - Codex code review and security review completed with no findings. ### Before merge - [x] Tom selected the direct-C approach with vendored upstream source; [the wrapper alternative](https://github.com/block/buzz/pull/8381) is closed. Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
fea02d174d |
fix(ios): fetch profiles and avatars in notification extension (#8369)
A notification can arrive from someone whose profile the main app has never loaded. The iOS notification extension now fetches and caches sender profiles, channel details, and avatars itself. The main app supplies account configuration and Keychain credentials only. After resolving the message, the extension allows 1.5 seconds for sender and channel details. It uses available stale data when a refresh is slow or fails, delivers once, and lets late results update its private cache. Profiles, channel details, and avatar refresh checks share a one-hour freshness threshold. A verified avatar removal clears the previous image. Account removal prevents further use of that sign-in's cached data. Profile, channel metadata, and membership use separate bounded downloads, so an oversized roster cannot discard the smaller records. Sender enrichment can finish while channel requests are pending. A successful partial channel refresh atomically removes its old counterpart, preventing a mixed group identity or recipient count. Group-specific notification presentation still requires a verified recipient count. The [specification](https://github.com/block/buzz/blob/native-notification-enrichment/docs/native-notification-enrichment.md) describes the display, caching, and account-removal behavior. General persistent image caching in Flutter remains separate. ### Validation [Hosted CI](https://github.com/block/buzz/actions/runs/38780108487) passed on the final commit, including all Flutter shards, Android, the full iOS release and simulator builds, and native controls/push-navigation tests. The [current-commit security review](https://github.com/block/buzz/pull/8369#issuecomment-6197508385) found no issues. The native package passed 159 tests, including new coverage for cache freshness, avatar replacement and removal, sign-in isolation, concurrent storage, late results, single delivery, bounded downloads, and SVG validation. A regression test exposed identical HTTP authentication event IDs for requests in the same second; each request now includes a unique nonce, matching the Dart and CLI clients. Full local CI passed before that small authentication fix. The complete native suite and signed device build passed again on the latest commit, including response-size limits, overlapping avatar downloads, canonical relay-origin matching, independent profile/channel refreshes, account-key changes before and during requests, and a single initial snapshot read across 64 accounts, signed IPv6 avatar authorities, and partial channel refreshes. Focused Flutter regression tests passed 198 tests with three configuration-specific skips; seven avatar widget tests also passed. Flutter analysis is clean, and the notification extension builds for the simulator. On an iPhone SE, the notification displayed the sender name, avatar, channel, and message before the updated main app was opened. Repeat notifications used a byte-identical private cache. Three notifications delivered together displayed the avatar after the authentication fix. The next notification also displayed correctly after terminating the extension process. The existing generic-push design can select the same newest unread message for several pushes; this change does not alter message selection.  Authenticated HTTP avatar fetching and persistent HTTP cache reuse also passed on the SE. The first notification downloaded a relay thumbnail. After terminating the extension and expiring its separate avatar entry, the next notification displayed the avatar and refreshed that entry without another media request in the relay logs. The main app remained unopened throughout. The original app build and test profile were restored afterward. Automated tests cover slow and failed requests, stale images, removal, and account revocation; those cases have not all been exercised on the phone. ### Before merge - [x] Verify authenticated HTTP avatar fetching and cache reuse on iOS against a working relay media service. <!-- buzz-review-completed --> --------- Signed-off-by: Tom Brow <tomb@block.xyz> |