mirror of
https://github.com/block/buzz.git
synced 2026-11-07 01:41:06 +00:00
fbfbf3d64f028da95368f8cd5c80ca0f77e482d0
39 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1a30e51689 |
mobile: start new agents on the server-recommended model (#9151)
## Summary Builderlab's model catalog can now recommend a model and thinking effort for new agents (`recommended_selection` on `/v3/beekeeper/get-model-catalog`). The mobile create-agent screen now starts on that recommendation, so the default can change on the server without an app release. New agents start on the first of these that applies: 1. The model an agent link suggests, when the catalog offers it (unchanged). 2. The server's recommended model and effort. 3. The first Sonnet model at medium effort, as added in #9034. Used while a deployment sends no recommendation. 4. Blank. A setup resumed after a failure still keeps its saved choice. A recommendation that isn't in the returned catalog, or that is malformed, is ignored. Existing agents' saved models are never changed. The server only sends a recommendation when the deployment's catalog config sets `recommended_selection`. Until then, the app keeps falling back to Sonnet. ## Testing - Unit tests cover the recommendation beating the Sonnet fallback, a draft model beating the recommendation, and recommendations that should be ignored (unsupported effort, unknown model, missing effort, malformed, absent). - A new widget test checks that the form opens on the recommended model and effort instead of Sonnet. - `just mobile-check` and the full mobile suite (`flutter test --dart-define=BUZZ_PUSH_GATEWAY_URL=https://push.example`) pass locally. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c848b6dcad |
fix(mobile): offer a new identity beside saved keys on unclaimed accounts (#9107)
## Summary This fixes a dead end in mobile Builderlab sign-in. A new account (unbound, with no backups) on a device that already had saved identities could only pick one of those identities. Any saved key forced the picker, and creating a key required the device to have none. If the service then definitively rejected binding the chosen key, the app showed "Let's try that again" with no way forward: the chosen key stayed in the account's slot, so every retry and every launch re-sent the same rejected bind. For an unclaimed account, the picker now also offers **Create a new identity**: - Creation writes only this account's empty identity slot, so every saved and device key is kept. - Creation is offered only when every current device key was among the choices shown, so a new identity never silently passes over a key the user didn't see. - Bound accounts, accounts with backups, pairing and pending import never offer it. When binding a chosen key is definitively rejected, the blocked screen offers **Choose a different identity**: - It appears only for a 400, 404, 409 or 422 from `/verify` in the current attempt, for the current account and pubkey, and never during pairing, whose transferred key is the only choice. Evidence from an earlier attempt is cleared before each bind. A challenge rejection, a 403, a 503 or any other ambiguous failure offers nothing. - Before releasing the slot, it rechecks the account: still unbound (`/current` and status), no backups, the same subject, and a slot that still holds that pubkey without a receipt. - It releases the slot only when another durable copy of the same key exists (the device inventory or another account slot), then verifies the slot is empty. No key is deleted: only the account's pointer to the rejected choice is cleared. A failed or refused release keeps the slot and the choice, so it can be retried in place or after a restart. - After release, the picker runs again and offers the saved keys plus creation. The recovery code lives in `hosted_flow_identity_recovery.dart`, a part of `hosted_flow.dart`, to stay within the mobile file-size limit. ### Related issue None found. ### Testing **Physical iPhone, a signed release build of `1fb633d55` installed over the existing app (keychain and saved identities kept):** a saved identity was rejected, then Choose a different identity, the picker, Create a new identity and Join your community. Joining then completed. This head differs from `1fb633d55` only in the guards for leaving, pairing and a failed or refused release listed below, which this journey does not reach, and in no UI code. The status of that phone's original rejection was not captured, so it is not confirmed to be one of the rejections above. The 409 case is covered by tests. **Tests (red on `main`, green here):** - Identity service and store: creation beside offered keys; refusal for unseen or partly offered device keys; release when another copy exists; refusal for a sole copy, a binding, a backup, a receipt, another account or an unusable recheck; failed delete and retry; a cancelled attempt. - Onboarding journeys: create beside saved keys; select, rejected bind, restart, choose a different identity, create and bind; ambiguous 503 and 403 failures; challenge rejection and stale evidence. - Release boundaries: an account switch at `/current`, at status, or between the recheck and the delete; cancelling between the recheck and the delete; leaving the picker before a late creation; an unreadable device inventory; leaving while a release is in flight; a failed delete retried in place and after a restart; a release refused because the inventory is unreadable, then retried in place; a rejected paired key. Each keeps every key, leaves the new account's slot untouched and leaves a cancelled flow idle, and a retry converges. - Mutating the creation guard, the generation check, the binding recheck, the store's receipt check, the account and cancellation checks before the delete, or the device-inventory error handling fails a test. #### Screenshots Physical iPhone at `1fb633d55` (frames from a screen recording; saved public keys redacted): | Rejected bind | After Choose a different identity | After Create a new identity | | --- | --- | --- | |  |  |  | Widget-test renders of the same screens (the real `HostedFlow` against a mocked account service, with a fixture key; the square at the top left is the test renderer's placeholder for an asset): | Picker on an unclaimed account | Rejected bind | | --- | --- | |  |  | 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: John Tennant <jtennant@squareup.com> Co-authored-by: Xander <ff30c859b96bd5233ffaf5d071200458f1274bd0dab03589214cf1055198491d@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
af54258f19 |
fix(mobile): streamline identity setup and community selection (#9070)
Selecting a saved community now connects only that destination, and **Find my communities** remains available through **Switcher → Add Community**. Failed destinations are named; completing nested Add/Find opens the selected destination without resuming the outer batch. Fresh hosted account setup now creates and backs up an identity automatically through email sign-in, invites, community links and community creation when the resolver confirms there are no account-bound, backed-up, saved or device identities. Welcome has one email sign-in entry. Authenticated import, existing-key recovery and selection retain their explicit paths; pending import survives restart and attaching a destination. Backup retry retains the same identity. Web binding retains explicit identity creation and its browser-return behavior. Self-hosted setup is separate and explicit. New-key creation still requires platform sealing support; passive account restoration never creates, binds or backs up an identity. **Account tradeoff:** setup cannot discover a key stored only elsewhere and never associated with this account. Such an account receives a new backed-up, bound identity automatically. Importing a different old key afterward fails with a binding mismatch; this flow does not undo that account binding. There is no separate pre-authentication import button. Pending import now survives retained community/web-bind link decoding: internal import metadata is stripped before strict public-link parsing. Once an identity is saved and resolved, the import request is retired before binding or backup, so restart resumes the existing key. Passive restoration also repairs stale import intents written by earlier builds without creating, binding or backing up a key. Validation (`f114c0e228`): - All 30 hosted onboarding journeys pass. Import coverage includes invite-to-community-link retention, restart before import, failed-backup restart, successful-backup restart, and an older stale import intent. - Three isolated production mutations (decode filtering, import completion persistence, passive stale-intent handling) each fail the relevant journey; restored control passes. Earlier auto-creation and nested-selection mutations also failed their relevant journeys. - Carl reviewed both P2 fixes and the final commit with no blockers. Carl and Pinky previously cleared the shared setup behavior and Android parity. - Wes confirmed Android sign-in and fresh account creation on the preceding build `be5690c684`. Android/iOS builds and earlier manual acceptance do not establish native acceptance of the new import/restart fixes. Installed devices remain on that preceding build. **Remaining acceptance:** current-head CI and native validation of the import/restart edge cases. Required publication hooks passed, including mobile analysis/tests, branch-skew and file-size checks. The existing iOS 16 sealing limitation remains: it cannot create a fresh hosted identity. No membership/ownership repair, denial-classification change or floating sign-in-control fix is included. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> |
||
|
|
ec21753e1b |
Make mobile New DM and Add members pick recipients like mention autocomplete (#9065)
The mobile New DM picker listed the first 50 relay profiles alphabetically, mostly identities the viewer doesn't know. It ran its own debounced directory search with no cache, and its rows disappeared on every keystroke until the search returned. Mention autocomplete in the composer already solves the same problem well, so New DM and Add members now use its implementation. Add members was reusing the same New DM directory search. ### What changes - **Same candidates and order as mention autocomplete.** The chooser's candidate building, ranking, row stability, 200 ms debounced search and search cache now back both surfaces. New DM is treated like a stream with no members yet: the viewer's own agents and agents shared with them appear up front, people are found by name, and agents that couldn't answer the viewer are left out. Typing a name shows the same people in the same order as `@name` in a stream. - **Results stay on screen while a search runs.** A query searched before shows its people immediately, and a longer query starts from the shorter query's results. A failed search keeps its rows and adds a retry row below them. - **Presentation matches the mention rows.** Rows show the name, and agents show "agent · managed by …". Npub lines are gone. - **Sheet chrome.** The title is "New DM", like the menu item that opens it. Both pickers keep their grouped cards but drop the "People and agents" and "Selected" section titles and the empty header bands above them (selected recipients stay in their own card at the top with checkmarks on iOS, as chips on Android). The native iOS picker now focuses search once it appears, which iOS used to ignore. In New DM a pick clears the query, as an inserted mention ends its query. - **Add members keeps what is specific to it.** Existing members and members just added are never offered, names are still compared with the channel roster so a same-name candidate is told apart, the query stays after a pick so several matches can be added in a row, and agents that don't answer the viewer can still be added, since a channel can hold them. While the WebSocket reconnects, the relay agent directory and its owners keep their last read in the community instead of going empty, so shared agents already found stay eligible in both New DM and the composer chooser. The debug-only offline preview directory and the old directory listing and search providers are removed, since no picker reads them any more. ### Validation Added tests: the New DM widget journey now drives the real mention search through a fake relay session. It covers the empty-query state, the debounce, rows kept while a longer query searches, cached results shown again at once, selection, chips and submit. The layout journey covers the 50-row cap, and the native-bridge journey covers title, rows without key detail lines, the selection limit and failure handling. The full `mobile/test/features/channels` suite passes. Automated app run (iPhone 17 Pro simulator, iOS 27, debug build, Remote Agents Pilot community): opened New DM from the menu, confirmed the keyboard comes up with only the viewer's own agents listed, searched for people and an agent, selected both and confirmed the query cleared after each pick. Also exercised the composer mention chooser (same results and order for the same query) and the shared Add members sheet (plain style, autofocus, search, query kept after a pick). Captures show real community members, so they're shared internally rather than here. The DM was not actually created, to avoid a stray conversation; submit is covered by the widget tests. 🤖 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> |
||
|
|
dafe31bfe5 | feat(mobile): persist frontier-only NIP-RS read state across devices and restarts (#8998) | ||
|
|
33eaa73c83 |
fix(mobile): let users sign out after leaving all communities (#8993)
## Summary Fixes a dead-end in signed-in mobile onboarding: after leaving every community, Back kept returning to the same Add a community screen without a way to end the account session. Show “Signed in as [email]” on that screen. Back asks “Sign out of BuilderLab?” with the email; Cancel keeps the session, and Sign out returns to the welcome screen while retaining saved Buzz identities. Nested Back and Add community from an existing session keep their navigation behavior. ### Related issue None found in the existing issue and PR search for onboarding sign-out. ### Testing The new widget journey failed before the change and passes through the real onboarding widgets, account client, storage and flow with external HTTP/browser boundaries mocked. It covers nested Back, Cancel, system Back, durable session removal, retained identity/backup, and starting email sign-in again. The existing hosted onboarding journey also covers accounts without an email. On iPhone 18 Pro simulator (iOS 27), rendered the production onboarding screen and native confirmation using a disposable account fixture. Live browser account selection has not been verified; this clears the app session, not website cookies. To reproduce on an auth-enabled mobile build: sign in, leave all communities, check the displayed email, tap Back, and choose Cancel. Tap Back again and choose Sign out. The yellow welcome screen should appear, with email sign-in available. ## Screenshots Before, from the original reported phone screen: <img src="https://raw.githubusercontent.com/block/buzz/07440b3b10c3f48a6fd19c923f2ee5ba66d574f0/pr-8993--before.png" alt="Before: account state not shown" width="300"> After: Signed-in landing and native sign-out confirmation, using a disposable account: <img src="https://raw.githubusercontent.com/block/buzz/a4e18c85bc3a19d971b08ff595ba6b081c67e20f/pr-8993--landing.png" alt="Signed-in landing" width="300"> <img src="https://raw.githubusercontent.com/block/buzz/a4e18c85bc3a19d971b08ff595ba6b081c67e20f/pr-8993--confirmation.png" alt="Sign-out confirmation" width="300"> --------- Signed-off-by: Aaron <f425b86c3b780d43f8d9cf1802429d04dd985bf92eac44fca9468b5ad3887c5e@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <jtennant@squareup.com> Co-authored-by: Aaron <f425b86c3b780d43f8d9cf1802429d04dd985bf92eac44fca9468b5ad3887c5e@buzz.block.builderlab.xyz> |
||
|
|
344f007f36 |
Add mobile Bestie onboarding and fix remote agent presence (#8957)
An empty Bestie slot cycles still posters from Fuzzies, Gloopies and Figgies every five seconds, without creating video players. The next poster is precached; reduced motion and hidden-screen pauses are preserved. The launcher uses the neutral “Bestie” label until a selection is available, then “Chat with …”. For supported communities with no owned agents, tapping opens Create agent directly. A successful first-agent setup saves that agent as Bestie without replacing an existing preference; existing-agent and unsupported-community paths keep the Agents list. Verified Builderlab registrations can show sleeping remote agents as Online. Owner-scoped saved runtime availability now prevents a known absent or deleted runtime from returning Online after Builderlab sign-out or a fresh load, even when another community retains its registration. Validation: mobile static checks; 4,274 Flutter tests passed with hosted flows enabled; launcher → cancel → create → Home “Chat with Scout”, including encrypted persisted selection; sign-out, fresh-load, owner isolation and deletion regressions. Restoring the old carousel produced three video-player requests; removing the saved-availability guard reproduced incorrect Online after sign-out. The earlier iPhone recording below shows the original carousel and entry flow and stops before creation. The new poster-only behavior and completed creation are covered by widget tests, not a new device recording. https://github.com/user-attachments/assets/94fcff72-967a-44c2-99d5-27c715e196a2 --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
76bc66acfc |
mobile: hold a queued list read for the 10s gap (#8958)
## Summary Follow-up to [#8910](https://github.com/block/buzz/pull/8910) for the two optional notes from its review: - **A queued list read now waits out the 10s gap** ([note](https://github.com/block/buzz/pull/8910#discussion_r4447689472)). If a change comes due while a slow read is still running, the read is queued. When the slow read finished, the queued one used to start right away, even if the gap after the previous read hadn't passed. That broke the "reads start at least 10s apart" rule: a read could start 9s after the one before. The queue now drains through `_readListIfChanged`, which holds the read until the gap ends. If the finished read already covered the change, nothing is read. - **A test for a partly failed unread catch-up** ([note](https://github.com/block/buzz/pull/8910#discussion_r4447689485)). Previously, deleting the line that retries a catch-up when only some channels' fallback reads failed left the suite passing. ### Related issue N/A. Follow-up to [#8910](https://github.com/block/buzz/pull/8910). ### Testing - New step in the open-list journey: the relay answers each of a read's two queries in 7s. A change that comes due while that read runs is read once, after the gap, not as soon as the slow read ends. Restoring the old drain fails the step. - New step in the resume journey: after a reconnect, the batched catch-up fails over to per-channel reads, and one channel's read is refused. That channel turns unread on the retry, and nothing more is read over the next 10 minutes. Removing the partial-failure retry fails the step. - `FakeRelay.holdQuery()` holds HTTP queries, so a test can make a read slow. - The full mobile suite passes, and `dart format` and `flutter analyze` are clean. - This is a timing-only change to background list reads, with no UI change. The journeys above drive the real notifiers against a fake relay. I did no device run. 🤖 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> |
||
|
|
009dc103f6 |
perf(mobile): read the channel list after live changes, not every minute (#8910)
## Summary While the app is open, a 60s timer reads the whole channel list again: every membership roster, the channel metadata, and the unread catch-up. On the test account that is **about 4MB of JSON every minute** on the 12 mini, about 240MB an hour, whether or not anything changed. The live subscriptions already deliver what the list is built from, so this PR removes the timer. The list is read again when a live event says it may have changed: - a relay notice that tags this reader: added to a channel or a new DM (kind 44100), removed from one (44101), or a DM hidden or shown on another device (30622) - a metadata edit on a listed channel (kind 9002, now part of the live filter) - a system notice on a listed channel that changes the channel itself: archived (including the relay auto-archiving an idle ephemeral channel), unarchived, deleted, visibility, topic, purpose, TTL and similar. Member joins and leaves don't count. The read starts 2s after the last such event, and no later than 10s after the first, so a burst costs one read. Reads start at least 10s apart however busy the relay is. A read that fails is retried with backoff (5s, doubling up to 5 minutes), including the read after a resume or reconnect. Nothing is read in the background: launch, resume, reconnect and pull to refresh read the list as before. Live subscriptions ask for no stored events (`limit: 0`), so nothing sent while one is down is replayed. So: - Each list read opens the notice subscription before it reads, and the read covers anything sent before. - If the relay closes the notice subscription or a channel chunk for a reason that may clear (busy, rate-limited, timed out), the relay session reopens it itself, as before. The channel list now hears about that through `subscribeWithStatus`. - If the relay refuses one as over its subscription limit, only that REQ is retried, after a jittered backoff (5s, doubling up to 5 minutes). The backoff starts over once a reopened subscription has stayed up for a minute. Any other refusal (`restricted:`, `invalid:` and so on) can't succeed unchanged, so it is left alone until the next connection, as on main. - A reopened subscription that is still up when its read comes due counts as a change, so the list is read once to catch up on what it missed. If the relay admits a REQ and then closes it again, it triggers no reads. Known gaps, accepted: deleting a channel or kicking a member sends this reader no notice, and archived channels have no live coverage, so an unarchive done on another device isn't seen. An edit to a newly added channel that lands in the round trip between reading its metadata and its live subscription starting is also missed. The list catches up on the next launch, resume, reconnect or pull to refresh. Archiving or unarchiving on this device re-reads the list at once, as before. ## Measurements iPhone 12 mini on Wi-Fi against the internal relay, app open and idle, instrumented profile build (HTTP response bodies after decompression, websocket text in). Measured on an earlier revision of this PR, which read on a 60s tick only after a change. Idle behavior is the same now (no change, no read). | 10 idle minutes | main | this PR | | --- | --- | --- | | Channel list reads | 10 | 1 | | HTTP queries | 40 | 5 | | HTTP response bytes | 39.9MB | 4.0MB | | Websocket bytes in | 4.4MB | 1.1MB | On main, every tick read memberships (348KB), another 310KB query and the unread catch-up (3.3MB). With this PR, the only read in the window came right after another device hid or showed a DM (kind 30622), which is exactly the kind of change it should pick up. Sizes are JSON after decompression; the relay gzips responses, so wire bytes are smaller (not measured). [#8901](https://github.com/block/buzz/pull/8901) separately shrinks each read's unread catch-up; the two are independent. ## Testing - New journey test: with the app open and nothing changing, nothing is read. Adding the reader to a channel, renaming a listed channel, archiving or auto-archiving one and removing the reader each cause one read about 2s later, and a member joining causes none. A change right after a read waits out the 10s gap. A steady stream of changes is read after 10s and then no more than every 10s. A burst costs one read. A failed read is retried with backoff and then stops. - New journey test, for the notice subscription and for a channel chunk: the relay refuses it twice as over its limit while the list changes. It is retried with backoff, and once it has stayed up the list is read once to catch up. Later the relay drops it, and having stayed up it is reopened within about 5s and catches up again. Then it delivers live. The relay then closes it as busy while the list changes, and the list catches up once it is back; closed again right after reopening, it triggers no read until it stays up. Refused as `restricted:`, it is not retried. - The resume journey: a reconnect read and a resume read each fail once while a channel is added, and each is retried and shows the channel. - Each guard was removed once to confirm a test fails: the debounce, the max wait, the minimum gap, the read retry (including after reconnect and resume), the catch-up reads after a retry or a relay-session reopen, the still-up checks, the backoff restart, the background check, retrying only capacity refusals, and clearing refusals on a new connection. - The terminal-rejection tests expect a chunk refused as `restricted:` never to be retried on that connection or to cause a list or history read, and a new connection to retry it at once. - `just mobile-check` and `just mobile-test` pass. - Device: on the 12 mini, 10 idle minutes with the app open, before and after (table above). 🤖 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> |
||
|
|
fa395e9e80 |
feat(mobile): open a prefilled Create agent form from Builderlab agent cards (#8912)
Builderlab agent cards (printed for the event, and generated at the kiosk) carry a QR to a share page, `app.builderlab.xyz/a/<id>`. Its **Add to Buzz** button opens `buzz://agent/new?card=<id>`, which no Buzz client handled until now. This PR makes mobile handle it. ### Behavior - Opening the link shows the Agents page, then a Create agent form prefilled from the card. Everything stays editable, and nothing is created until the user taps Create. - The app fetches the card's `agent.json` from the avatars CDN for this build's Builderlab environment (production or staging). The link carries only the card id, never a host, so the QR stays small no matter how long a card's instructions get. - `agent.json` is validated against the card schema. `id` (matching the link), `name` and `avatarUrl` (an https URL) are required. `instructions`, `model` and `reasoningEffort` are optional (published cards don't have them yet) and left for the user when absent. A missing required field or a wrongly typed known field rejects the card; any other key is ignored whatever its type, including `v` and `description` (Buzz agents have no description to prefill), so Builderlab can add fields without breaking released apps. The model and effort are preselected only if the live catalog offers them. The card id is opaque: any non-empty id is accepted and percent-encoded into the URL. - The fetch uses the same limits as image downloads, now shared in `bounded_fetch.dart`: aborted after 20 s without a byte or 5 min overall, and rejected over 2 MB. `MediaImageProvider` now calls the same helper, with no behavior change. - The link waits for sign-in, then reloads the registry for the signed-in account before anything else. It's dropped with a message if the card is unavailable (403/404), invalid, can't be loaded (any other response), or an agent setup is unfinished, including a setup saved before a sign-in the link triggered. - A link that arrives while Agents shows only its list replaces that page instead of stacking a second one. If a form or sheet is open there, the link is dropped with "Finish or close what’s open in Agents, then reopen the link." and the open work is kept. ### Demo Simulator (iPhone 17 Pro, debug build of this branch plus the sign-in fix from [#8914](https://github.com/block/buzz/pull/8914) and the hidden debug banner from [#8915](https://github.com/block/buzz/pull/8915), signed in to Builderlab, remote-agent-pilot), recorded in one take with idle time trimmed: the card's share page in Safari, Add to Buzz, the prefilled Create agent form, a model, thinking level and instructions, Create, then a message and the agent's reply. https://github.com/user-attachments/assets/e66aae17-884a-4e68-a1b8-ecb40562be1d ### Validation Added tests: - Table tests for the link shape (only `?card=<id>` with an opaque id; the inline `#` form, extra parameters and dot segments are rejected) and for reading `agent.json` (a published card, a card with instructions and model defaults, unknown keys of any type ignored, including `v`, `description`, arrays, objects and null, and invalid cards: missing or mismatched required fields, wrong types, and non-https or relative avatars). - Table tests for fetching: the request goes to this build's CDN without following redirects, with the id percent-encoded as one path segment; 403/404, a redirect, a 500, a network error, declared and streamed oversize bodies, non-JSON and a JSON array each give a specific message. The image loader's existing stall, deadline, cap and cancel tests now cover the shared helper. - A journey test from link to Create against a fake CDN: prefill and edit, then submit; an unavailable card; a failed model catalog whose Retry then preselects the card's model; a Builderlab account change while the card loads, which drops the draft; a v1 card with no instructions and an unknown model; an unfinished setup; a link arriving while a Create form with edits is open, which keeps the form and its edits; and a link arriving while Agents is open. Disabling the https or id check makes the table test fail, and removing either the account recheck or the loaded-catalog check fails the journey. The journey runs through the production root scope (removing its link handler fails it) and runs in CI's configured-authentication-domains step. Full mobile check and test suite pass locally. Live data: a real staging card's `agent.json` (`av_MioTAG6dm7q3Zcm0UV2j8A`) has the v1 shape the table test uses, and an unknown id returns 403 from the CDN. Live run on an iPhone SE (build of 63ed54c749, Builderlab sign-in enabled, remote-agent-pilot, signed in as builderlab@tombrow.com) with a real production card, [ToastyMacbeth](https://app.builderlab.xyz/a/av_lrmRlJR6ylrQKvJkoLWCOQ) (a v1 card with no instructions and an extra `description` key): - Opening `buzz://agent/new?card=av_lrmRlJR6ylrQKvJkoLWCOQ` from a cold start opens Create agent prefilled with the card's name and avatar. - After choosing a model and adding instructions, Create adds the agent with the card avatar, and it replies in a DM. - An unknown card id shows "This agent card isn’t available." | Prefilled | Created | Reply | Unknown card | | --- | --- | --- | --- | | <img src="https://raw.githubusercontent.com/block/buzz/b9222db494758c3e536b8e2011032ba7cec86daa/pr-8912--01-prefilled.png" width="200"> | <img src="https://raw.githubusercontent.com/block/buzz/b9222db494758c3e536b8e2011032ba7cec86daa/pr-8912--02-created.png" width="200"> | <img src="https://raw.githubusercontent.com/block/buzz/b9222db494758c3e536b8e2011032ba7cec86daa/pr-8912--03-reply.png" width="200"> | <img src="https://raw.githubusercontent.com/block/buzz/b9222db494758c3e536b8e2011032ba7cec86daa/pr-8912--04-unavailable.png" width="200"> | Before this PR, the app had no handler for `buzz://agent/new`, so the link did nothing. A separate widget test covers a link that needs Builderlab sign-in first: once sign-in lands, a setup saved for that account blocks the card, which is never fetched. Removing the registry reload fails it, as does removing the busy-Agents guard for the journey step above. ### Before merge - [x] Human live check waived under the repository owner's standing testing exception. In its place, agent-driven full-app runs exercised the flow end to end: the iPhone SE device run and the simulator demo above (a real card's share page, **Add to Buzz**, the prefilled form, Create and a reply from the agent). Limitation: those runs predate [ |
||
|
|
da93ce3f62 |
fix(mobile): open the public Builderlab login the iOS bridge allows (#8914)
On main, "or sign in with email" on iOS fails immediately with "We could not finish connecting. You can try again." No Builderlab page opens. [#8700](https://github.com/block/buzz/pull/8700) changed the iOS browser bridge so the public account hosts (`app.builderlab.xyz`, `app.builderlabstaging.xyz`) may only open `/auth/native`. The Dart side still opens `/api/goose/v1/auth/login` for them, so the bridge rejects every public sign-in before presenting a browser. Every iOS build since #8700 is affected; organization sign-in on tenant hosts is not. The matching Dart change was Wes's [#8712](https://github.com/block/buzz/pull/8712). It was merged into the #8700 branch, but main's squash of #8700 kept only the bridge half. This PR restores #8712 on top of main: - The public account opens `/auth/native` (state, S256 challenge, callback and retained invite unchanged, without the CLI-only `type`/`product` parameters). Organization tenants keep `/api/goose/v1/auth/login`. - A saved session whose account still needs web setup goes through the same browser handoff, keeping its subject and destination. Android has no path allowlist in its browser plugin, so it needs no change. ### Validation - Reproduced on an iPhone 17 Pro (iOS 27) simulator with a main-based debug build: "or sign in with email" fails instantly. With this change applied, the same build shows the system "Buzz wants to use app.builderlab.xyz to sign in" prompt, then the login.builderlab.xyz "Welcome back to Buzz" page, which confirms production serves `/auth/native`. - Restored #8712's tests on top of main's current tests: the browser test checks the public login URL and its exact parameters, and the hosted invite journey runs for both a fresh account and a saved account that still needs setup, asserting `/auth/native` and that the invite is retained. Without the Dart fix, these fail. - Mobile format and analyze pass; the hosted onboarding journeys and `shared/hosted` tests pass with the CI auth-domain configuration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Tom Brow <tomb@squareup.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> |
||
|
|
daeb463f37 |
fix(mobile): prompt Builderlab sign-in instead of labeling agents desktop-only (#8907)
Without a Builderlab session, mobile can't tell which agents run on Builderbot, so the Agents page labeled every agent "Manage on desktop", including remote ones. Now, when the community supports remote agents and there's no Builderlab session: - The Agents page shows "Sign in to Builderlab to manage remote agents" with a **Sign in** button (the same reconnect flow as Add). - Agent cards and the navigation subtitle drop the "Manage on desktop" label. - An agent's profile says to sign in to see whether it runs on Builderbot or desktop. ### Before / after iOS simulator, signed in to the remote-agent-pilot community with no Builderlab session. | Before (main) | After (this PR) | | --- | --- | | <img src="https://raw.githubusercontent.com/block/buzz/9be9cebedc8f5ff172e97a3df2cbf505a90e41c4/pr-8907--before-agents.png" width="300"> | <img src="https://raw.githubusercontent.com/block/buzz/9be9cebedc8f5ff172e97a3df2cbf505a90e41c4/pr-8907--after-agents.png" width="300"> | ### Validation - New widget test `agents_signed_out_prompt_test.dart` (runs with `BUZZ_AUTH_DOMAINS`, added to the CI list). - `just mobile-check` and the mobile test suite pass. - Checked the signed-out Agents page in the iOS simulator. buzz-review-completed (manual testing waived by Tom) 🤖 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> |
||
|
|
543ecf2b1f |
fix(mobile): list the same owned agents as desktop (#8857)
## Summary
Mobile's Agents modal listed agents that desktop does not show. Most
were agents created in the earlier Buzz app (this repository's
`desktop/`), which the owner had not imported into Buzz 1.0
([block/buzz-app](https://github.com/block/buzz-app)). @ completion
offered them as well.
Desktop lists an owned agent only when the owner's newest kind:30177
record for that agent has a `pluginId/typeId` `runtime` tag and a name.
Records written by the earlier app have no `runtime` tag. Importing an
agent on desktop republishes its record with one
([`listingsOf`](
|
||
|
|
83ec9c0e2b |
fix(mobile): show hosted community names consistently (#8872)
When a hosted relay has no metadata name, the hosted add paths save its host (`remote-agent-pilot.communities.buzz.xyz`) as the community name. Onboarding shortens that fallback to `remote-agent-pilot`. After joining, though, the community switcher, the remove dialog, the theme preview and the identity export showed the full host, so the same community appeared under two names. This change shortens the name in the model instead of at each call site. `Community` and `AddCommunityDeepLink` keep the saved value private, and their public `name` getter returns the display form, using the existing `HostedEnvironment.communityDisplayName` rule. Existing and future readers get the short name without opting in. Serialization still writes the saved value, so stored data is unchanged and no migration is needed. Custom names and non-hosted hosts pass through unchanged, as before. Community identity is unaffected because it uses `id` and `relayUrl`, never the name. Beyond the captures below, the same getter shortens the name in the iOS push notification subtitle, onboarding's "Adding …" progress text, the "You joined …" invite sheet, and the VoiceOver labels for removing and loading a community. ### Before and after | | Before (`main`) | After | |---|---|---| | Community switcher |  |  | | Remove dialog |  |  | | Theme preview |  |  | | Identity export |  |  | ### Before merge - [x] Kenny Lopez reviews the visible changes ([approved in Buzz #buzz-mobile](buzz://message?channel=9f18f3c6-b5e5-453d-b36d-65487913be0b&id=c8b01eaa3760e44c3b6583f2e02bec576d78679adfb364e5fe718cc0926bdc2d)). ### Validation - Captures come from iPhone 17 simulators (iOS 27) running `main` and this branch. Both builds used the same scratch-only seed with two communities whose saved names are their hosted hosts. The seed is not part of this PR. The relays were unreachable from the simulator, so the screens show loading placeholders. - Changed tests: the community switcher journey now uses a hosted host-name fallback for one community. It asserts the grid label, the remove tooltip and the dialog text show the short name, and it fails when the getter returns the saved value. The onboarding label table now asserts the model's `name` itself is shortened. The hosted-flow fixture records relay hosts instead of names, and the auth-domain tests look up communities by relay URL. - The full mobile test suite and `flutter analyze` pass. The CI step that runs with configured authentication domains also passes locally (113 tests). - Not captured: the push notification subtitle, onboarding's "Adding …" text and the invite sheet. Reaching them needs hosted sign-in or push, which the simulator doesn't support. 🤖 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> |
||
|
|
db4478e8d2 |
feat(mobile): organization sign-in for NIP-FI relays (#8700)
Block is turning on NIP-FI enforcement on the Block relay. Desktop gets organization sign-in in block/buzz-app#1424. Mobile can already attach NIP-FI assertions, but only for Builderlab-hosted communities, through the build's single Builderlab account. The dogfood build needs that account for public hosted communities, so it can't simply be pointed at the Block tenant. This adds a separate **organization realm**, configured at build time: - `BUZZ_ORGANIZATION_URL`: the tenant's HTTPS origin (for example `https://block.builderlab.xyz`). - `BUZZ_ORGANIZATION_RELAYS`: the exact relay hosts that trust that tenant. The two must be set together. The app refuses to start if they're malformed, overlap a hosted community, or reuse the hosted origin. Without them, nothing changes. For a configured relay: - The app keeps its own organization session and signs in through the browser at the tenant's existing login (`/api/goose/v1/auth/login`). Block SSO happens there. - It requests assertions from the same `/v1/buzz/identity/assertions` route that hosted communities use, and attaches them to the relay socket, HTTP, media, huddle and remote-agent requests. - Unlike the hosted path, it doesn't require an identity backup before the first binding. As on desktop, the tenant binds the member's existing key on first issuance. A key already bound to a different one is refused before any request is sent. - When signed out, it connects without an assertion and lets the relay decide. A bottom banner offers **Sign in**. After sign-in, the relay reconnects with a fresh assertion. Hosted communities keep their existing banner and flow. The tenant accepts only the registered `xyz.block.buzz.mobile:/auth/callback` return URL, so both realms share it. Each `BrowserAuth` owner leaves callbacks whose `state` belongs to another owner's attempt to that owner. ### Before merge - [ ] Kenny Lopez reviews the visible changes. ### Validation Added tests (`organization_relay_auth_test.dart`): - Configuration parsing and exact host matching. - An assertion is issued for the exact relay and key, then cached. - A different bound key is refused. - Signed out connects without an assertion. - Other relays never receive the assertion. - The shared callback is routed between the hosted and organization owners. Full `mobile-check` and `mobile-test` pass. RunnerTests pass locally except `testNativeNavigationTitleExpandsAgainAtTop`, which this PR doesn't touch. Added tests (`organization_sign_in_test.dart`, `relay_auth_isolation_test.dart`, `builderlab_account_test.dart`), each failing without its fix: - Concurrent account restores at cold start share one adopted account. - An exchange from a cancelled attempt never changes a newer sign-in. - Storage failures while clearing the intent or cancelling leave the busy state. - An unreadable saved organization session is replaced by signing in again. - A stalled organization issuer never delays a hosted relay's assertion. This test needs `BUZZ_AUTH_DOMAINS` and skips without it. CI's configured-auth-domains step doesn't list it yet, because that workflow change needs a token with `workflow` scope. Added tests also cover a restore after an offline start (the banner clears once a relay retry restores the session) and the iOS bridge allowlist (`/auth/native` and tenant login on Builderlab subdomains; other hosts, paths, ports, userinfo and fragments refused). Simulator (iPhone 17, iOS 27) with the dogfood defines against the production Block relay `buzz.block.builderlab.xyz`, on a WARP Mac: - The app starts with the organization configuration and connects to the relay while signed out, showing the organization **Sign in** banner. - **Sign in** opens the system sign-in session for `block.builderlab.xyz`, and the banner shows **Finishing organization sign-in…**. Cancelling returns to **Sign in** with no error. Before the bridge fix, no browser opened and the banner reported that sign-in did not finish. - The bottom banners (organization sign-in, hosted reconnect, and the sign-in recovery row) now sit above Home's floating Search/New bar, which used to cover **Sign in**. A journey test checks the banner clears the bar, and fails without the fix.  - The simulator run stopped at the system consent prompt (SSO does not work in the simulator). The app calls were driven through the VM service (the same `signIn()`/`cancel()` the banner buttons call) because the simulator could not be tapped. Device (iPhone 12 mini) against bb-block staging in enforce mode, where every probe without an assertion gets 401: - Signed out, the organization banner shows and the app stays responsive. Cancel at the consent prompt returns to **Sign in**, and retry works. - Block SSO through Okta completes, and the relay admits the reconnect with the assertion. - After relaunch the app connects, loads channel history, and shows no banner for over 60 seconds. - Before the restore fix, 2 of 4 cold starts showed "cannot use this community with this key" although the session was valid. With it, 8 of 8 cold starts connected. Known limitation: adding an enforcing organization relay by community link is tracked in #8791. The dogfood build enables this in squareup/buzz-releases (`tomb/dogfood-organization-sign-in`). App Store builds stay unchanged. 🤖 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> |
||
|
|
f59b326568 |
Fix mobile onboarding policy loading, browser cancellation and pairing feedback (#8874)
Mobile onboarding could reject valid large legal documents, stay busy after closing Android browser sign-in, and hide pairing-import errors behind the community-link form. This change bounds join-policy responses at 4 MiB while retaining 64 KiB for other responses, handles Android browser cancellation through the native lifecycle/channel, keeps Cancel sign-in usable, and shows friendly pairing errors on the landing screen with a live-region announcement. Identity mismatch checks remain intact. The browser-cancellation implementation incorporates the candidate from #8847 (`cb241b3f1`); coordinate landing to avoid duplicate work. This PR consolidates the three fixes exercised together during Android onboarding acceptance. ## Validation - Pixel 9 / Android 17, production hosted services, isolated debug installs. Human acceptance plus native UI, saved public-identity and restart checks as noted below. No iOS acceptance in this session. - Latest build and invite-first acceptance: base `a12ad1cdd29da8b2923eca5408e963a2659777ab` plus these patches. - Focused tests on that base plus patches: **88 passed** across hosted_journeys_test, hosted_admission_test, browser_auth_test, pairing_hosted_handoff_test and pairing_provider_test, with `BUZZ_AUTH_DOMAINS=communities.buzz.xyz`. - Earlier baseline: 81 focused cases passed plus one staging-configuration case passed separately (82 distinct cases across seven onboarding/recovery files); these are historical, not current-head certification. - Earlier policy change: 41 focused tests, changed-file analysis and isolated mutation check passed on the earlier base. Earlier browser candidate native instrumentation/mutation evidence was not rerun on this combined revision. - Android debug APK built and installed successfully. Mandatory commit/push hooks passed at `6442e5ff9`: analysis clean; mobile tests 4,047 passed / 34 skipped; additional package runs 4 and 27 passed; file-size checks passed. These broad tests ran through the required push hook, not a manually invoked suite. CI remains pending. ## Live testing report — October 31 | Journey | Result and evidence boundary | |---|---| | Fresh email signup / identity / hosted invite | Reproduced policy response exceeding 64 KiB; after fix, admission and restart passed. Profile/message visible locally; cross-account message receipt remains unproven. Tested at `cfcaef4e9f` plus policy patch. | | Browser close → retry → login → existing identity recovery → restart | Passed at `d14f33a170` plus patches; recovered the expected public identity and community. | | Pairing a different identity into an existing install | Rejected without replacing the original key. Initially hidden error became visible with friendly copy; Back and restart preserved the community at `b80217de3` plus patches. | | Fresh pairing → wait for desktop approval → sign-in → community → restart | Passed at `20742a236` plus patches; human confirmed approval gate, saved public identity matched. | | First-time desktop cancellation → welcome → fresh QR retry → community → restart | Passed at `20742a236` plus patches. Cancellation error remained raw `user_denied`. | | Android system Back while awaiting pairing approval | Human-confirmed in already-onboarded Add Community; first-time phone-side Back variation remains untested. | | Wrong pairing code → rejection → correction → completion | Human-confirmed at `a2540fbe3` plus patches. Exact error and same-session versus fresh-QR retry were not independently captured. | | Interrupt account onboarding at community picker → process restart → recovery → admission | Passed at `a2540fbe3` plus patches. Restart opened Add a community; Find my communities restored choices without another login. Human completed admission. No destination had yet been selected. | | Invite-first into fresh signed-out app → intended community → restart | Passed at `a12ad1cdd` plus patches; saved identity/destination matched. Existing member and existing browser session. Initial Android VIEW intent explicitly targeted the test package; ordinary HTTPS default app-link resolution and new-member admission remain untested. | | Malformed community input → error → Back → restart | Passed for `not a community`; original community preserved. `not-a-community` is a valid single-label hostname and was not counted as malformed-input evidence. | ## Remaining gaps and findings Not release certification. Pairing landing-error regression/mutation evidence is now recorded below. Current combined-revision native instrumentation has not been rerun; its limited scope is described below. Deferred behavior, not implemented here: - Wrong browser account after fresh pairing is correctly rejected, but Retry reuses that app account session; in-app account switching must preserve the transferred identity and destination. The live happy path used a manual browser-account switch and fresh install. - Pairing wait has no visible Cancel; tested system Back works. - Desktop cancellation exposes raw error wording. - Interrupted community selection requires an extra Find my communities step. Master checklist remains partial: seven categories exercised this session, signup/community creation has historical coverage, seven categories unexercised. New-member link-first admission, deeper interruption/server-commit recovery, expiry/revocation/network failures, key import, organization/self-hosted paths, phone-to-desktop transfer, eligibility gates, accessibility/app-link variants and iOS require separate coverage. Channel-discovery work is separate (#8869). ## Review checkpoint [Carl’s review](https://github.com/block/buzz/pull/8874#pullrequestreview-5724095754) found no code blockers at `6442e5ff9`. Carl also reviewed test-only follow-up `d43958653` and found no issues, accepting both validation gaps as closed. A subsequent review identified stale callback → resume cancelling a newer Android login. A narrow native fix and regression validation are in progress; prior readiness attestation is withdrawn pending this behavior change. - **Android stack check passed (Pixel 9, Android 17):** after fresh account sign-in, the human stopped at the community picker without closing/restarting. `dumpsys activity activities` showed exactly one MainActivity in Invite First task 567, with no CustomTab in that task or the full component list. ChromeTabbedActivity remained in a separate pre-existing browser task. This rules out a lingering duplicate activity in this run; it does not independently trace `onNewIntent` or establish all Android/browser variants. Binary production sources match `6442e5ff9`. - **Pairing regression/mutation passed:** extended the existing QR journey using the real encrypted NIP-AB import and hosted coordinator. Asserts visible friendly mismatch feedback, live-region wrapper, unchanged stored community/account identity, and Back to home. Reused the existing socket fake. Hosted journey + pairing provider files: 70 passed with configured auth domain; opt-out hosted journey file: 3 passed / 21 skipped; changed-file analysis clean. Isolated clone: control passed, hiding the landing error failed the visible text assertion, restoration passed. No production source change in this follow-up. - **Duplicate landing:** #8847 is superseded by this combined PR; its Android slice is included here. No merge performed. BrowserAuthPluginTest manually drives plugin hooks using an unmanaged Activity and fake browser launcher. It proves the plugin state machine only, not MainActivity wiring, real lifecycle order, process death, or duplicate app_links callback delivery. Current CI does not run Android instrumentation. Android ignores the debug ephemeral-login flag and shares browser cookies. Readiness checklist: agent review completed on production changes and test follow-up; agent exercised the changed behavior; human completed the documented Android journeys and final sign-in check. ## Stale callback follow-up (`b7aaaa788`) Addresses [the later Android lifecycle review](https://github.com/block/buzz/pull/8874#pullrequestreview-5724197312): a recognized callback now clears browser-dismissal tracking before rejecting a mismatched state. A stale callback cannot complete or cancel the newer login. The existing native journey now includes the intervening resume, successful current callback, and genuine browser dismissal after a stale callback. Only two native files changed; policy and pairing fixes are unchanged. Validation: 88 focused Dart tests passed. Android app and test APK build passed. On Pixel 9, the native plugin journey passed; in an isolated source/Git/build clone, removing the reset failed at the newer attempt's expected-zero-replies assertion; exact restoration passed. This proves the plugin state machine on Android, not a real late browser callback through OS lifecycle delivery. Carl reviewed `b7aaaa788` and found the fix correct and minimal with no blockers. Human browser-close/retry check remains pending; PR stays draft until that passes. Current-head CI is also required for merge. Accepted limitation: if a recognized stale callback arrives after the current browser tab has already gone, automatic dismissal is suppressed and the user may need the visible Cancel button. The callback cannot complete sign-in without matching state; Dart validation remains authoritative. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> |
||
|
|
a2540fbe3d |
Align Android profile setup with iOS steps (#8871)
Android profile setup now matches iOS: choose a display name, then an optional avatar using the existing dotted-circle placeholder and Image / Emoji / Animated editor. Back preserves drafts; Skip saves the name without an avatar, and failed saves retain retry/exit options. Validated on Pixel 10 and approved by Kenny; analysis clean, 4,064 mobile tests passed (8 skipped). Device preview uses no-write callbacks; preview-only files are excluded. | Display name | Avatar | | --- | --- | |  |  | buzz-review-completed Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
2ca77747d1 |
Fix mobile Add Community loading and membership restoration (#8796)
## Summary Open Add Community immediately from the post-login picker and return directly on Back, without membership restoration or repeated discovery during navigation. Completing Add restores the saved selection while keeping the newly added community active; failures retain progress and offer Retry. Follow-up to #8729. Mobile app update only; no relay deployment required. ## Testing `just mobile-install mobile-check mobile-test` passed (4,025 standard tests), plus 4,049 Builderlab-enabled tests on the merged revision. Production-path journeys cover hosted and self-hosted completion, partial restoration and Retry, persisted memberships, and the active destination. Removing the completion hook makes the new journeys fail. Independent agent review found no blockers. Native walkthrough and human verification remain pending. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
75854111b8 |
fix(mobile): retain invite keys and reject incomplete onboarding (#8753)
Self-hosted invite claims retain the generated identity in secure storage before the HTTP request and reuse it after a lost response or restart. Saving or reopening an existing destination retires the matching journal. Sign-out/removal deletes all matching pending key copies before removing the saved community, preserving the community for retry if cleanup fails. Pairing verifies that relay authentication completed before saving/activating a destination or reporting success. On iOS 16, hosted entry permits recovery and reuse of backed-up identities, including binding a recovered identity with a confirmed backup receipt. Paths requiring a new secure backup explain the iOS 17 requirement before generating or binding a new key. Self-hosted entry remains available. ### Validation - Initial focused entry/recovery files: 83 passed, 1 skipped. Review-fix focused storage, community, invite and hosted files: 82 passed, 1 skipped; extended invite journey: 17 passed, 1 skipped. - Final hosted journey extension: 20 passed, including both bound and unbound recovery with native sealing unavailable. - Isolated mutations removing invite retention, pairing validation, journal removal/re-entry cleanup or restoring the blanket platform gate fail their intended regressions. Restored controls pass. - iOS simulator build passed on the original integration base; final native/device acceptance remains outstanding. - Carl and three specialist reviewers found no source-review blockers at `323cec6ea2`. `a55a246857` adds analyzer fixes, copy cleanup and the recovered-unbound journey coverage. Reviewers did not run tests or the app. - Fixed a push-hook timeout in `4564b3562a`: idle serial queues now release completed futures, avoiding scheduler retention across widget tests while preserving in-flight ordering and failure isolation. The existing dispatcher journey fails promptly before the fix and passes afterward; Carl reviewed this delta with no blockers. Focused dispatcher/invite/storage checks passed, and all 20 hosted journeys passed with hosted auth enabled. - Mandatory pre-push checks passed at `4564b3562a` (scope, branch skew, file size, mobile analysis/tests and crypto package checks); GitHub head verified at that commit. ### Remaining acceptance Draft pending agent/device and human acceptance. Exercise interrupted one-use invite claim and retry across restart, sign-out then reopen without restoring the removed identity, rejected self-hosted pairing, iOS 16 backed-up identity recovery (including binding), and iOS 16 new setup blocked before key creation. Synthetic network/storage boundaries do not prove native durability or live server behavior. Hosted CI results are not claimed here. Optional follow-ups remain outside this patch: pruning provably unadmitted journals, malformed-record recovery, avoiding an iOS 16 dead-end device-key selection, and pre-existing removal/push-revocation ordering. This PR contains three existing-path fixes. The two organization-login fixes depend on #8700 and remain separate: its stale parent conflicts with main in unrelated relay code, and the required branch-skew hook prevents publishing that stack unchanged. This PR does not refresh that parent or change deployment policy. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> |
||
|
|
fae3f0346c |
test(mobile): keep late callbacks and fixed sleeps from failing tests (#8678)
## Summary Fixes three mobile flakes from the 21–28 Oct 2026 CI runs, in test code only: - In two iOS `RunnerTests` classes, late bridge callbacks no longer fail whichever test is running when they fire. - The waits on a busy test-host main thread now cap at 10 s. - Two Flutter avatar tests poll for the ready frame instead of sleeping a fixed 50 ms. This is one of seven PRs split out of the `flakey-tests` branch (siblings below). ### Related issue None found. ### Flaky tests fixed | Test | Failing runs | Cause | Change | |---|---|---|---| | `BuzzPushSnapshotEnrichmentTests` (RunnerTests) | 40362671989 (job 121712747802, main), 39681273328#1 (job 119504560518), 40658588384 (job 122715950541, PR #8640) | The 1 s waits ran out while the test host's Flutter app held the main thread, and the test returned and deleted its temp directory. The bridge callback's assertions then ran later and were blamed on whichever test was running (`ProfileSetupRecoveryTests`, `IdentityFilePluginTests`). | Callbacks only record the value and fulfill. Every assertion, including the `isRestricted` checks, runs after the wait, so a late callback writes into a dead local. The seven waits cap at 10 s (`callbackTimeout`). | | `ProfileSetupRecoveryTests` `testStartedEditorWithoutReadyRestoresProfileAndAllowsRetry` | 40585805357 (job 122477274956, PR #8568) | Engine release and editor dismissal must both happen within 2 s, on the main thread the host app keeps busy. | Both predicate waits cap at 10 s. | | Flutter `channel_detail_page_test.dart` "profile sheet shows the poster before autoplay and restores it on tap" and `settings_profile_header_test.dart` "shows the poster until the animated avatar is ready" | 40368315739 (job 121732711851, main) | After the image response is released, both tests slept a fixed 50 ms of real time before `pumpAndSettle`. A slower real decode left no frame scheduled, so the ready key was missing. | A new `WidgetHelpers.pumpUntilFound` pumps in 10 ms real-time steps until the ready key appears, capped at about 5 s. | Local evidence is from the original `flakey-tests` commit (53aaec97c), Xcode 27.1 on the iOS 26.2 simulator: - Enrichment, with `Self.complete` delayed 1.5 s and 4 s: 6 failures before, 107/107 after. With a 12 s delay, every failure lands in its owning test. A mutated error code is still caught. - Profile setup, with the engine release held 4 s: 2 failures before, 4/4 after. A leaked engine still fails. - Avatar tests: under load, 94/96 before and 96/96 after. With a 200 ms image delay, 0/5 before and 5/5 after. A widget that never signals ready still fails. Not included: - The planned `AppDelegate` / `Runner.xcscheme` change that keeps Flutter out of the RunnerTests host. It did not work locally: restored scene sessions and the `UIMainStoryboardFile` fallback, and a normal launch came up black afterwards. - `IdentityFilePluginTests` `testSyntheticDocumentsReadInPlaceAndRejectInvalidOrOversize` (40441255307#1). It is left unchanged and stays open. ### Verification This branch was pushed without local re-verification, by the user's choice: local test runs were overloading the machine and failing for that reason. **Hosted CI is the check.** It was pushed with `git push --no-verify`, because the pre-push hook runs the Flutter tests. - Every file is byte-identical to its final version on `flakey-tests` (67185df6a). - Checked for this PR: `git diff --check`, and a conflict-free merge with all six siblings onto `main`. - #8613's flake-check lane will repeat the two changed Dart test files. ### Readiness Test-only change. AGENTS.md's readiness steps (agent review, then human confirmation) are still outstanding, so `buzz-review-completed` is not set. ### Merge order Independent of the other six. It merges cleanly onto `main`, and with every sibling, in any order. ### Sibling PRs Split from `flakey-tests` (one PR per area): #8676 (PostgreSQL lease + lane), #8677 (relay E2E helper + Windows shell), #8679 (Tauri apt cache + Windows Rust cap), #8680 (desktop product races), #8681 (desktop E2E state gates), #8682 (desktop E2E composer + row actions). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Jitter <2b2e6415e748c35180847a37aadae06a11e30dddc76b784e7fd9354c4eb42e7a@buzz.block.builderlab.xyz> |
||
|
|
0cde1fe2f4 |
Make mobile Agents a full-page owned-agent directory (#8693)
Replace the mobile Agents sheet with a full-page directory of verified owned agents. Add dedicated creation and avatar pages with catalog-backed model selection, reuse the profile emoji/photo/camera controls, and expose Builderbot model settings while labeling desktop agents “Manage on desktop.” Share one account-scoped model catalog across editable cards and retries. Choose Bestie from the agent profile using the shared encrypted main-agent record (text-only action, filled star once selected), and move confirmed deletion to a top-left trash control with native iOS glass. Include uploaded avatars in owner registrations so desktop can display them. Validation: mobile analysis and all 3,958 hosted-enabled app tests passed; default-build tests run by the push hook. Earlier iPhone build installed and iterated with the user; the current iOS build succeeds, but final changes still need a device pass. Independent review found no new blockers. Actual camera capture and live Bestie sync remain unconfirmed; the existing shared `main-agent` wire format and Bestie terminology are retained. ### Snapshots Widget-harness captures with test data (not native iOS chrome). | Agents directory | Image avatar editor | | --- | --- | |  |  | --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
e922dc08ff |
Unify mobile onboarding and community entry screens (#8687)
## Summary Use consistent fullscreen screens for identity setup, community selection, joining, and Add Community. Restore all memberships and open the selected community; share the community grid with the switcher. Add QR-first choices, animated link entry with 12px keyboard clearance, a two-finger double-tap pairing toggle, and the centered reversing bee loader. Preserve cancellation, Back, and reduced-motion behavior. Add intentionally omits community creation and Builderlab sign-in: this route follows sign-in and the existing-community picker. ### Related issue Related: #8628 (native profile setup). No duplicate fullscreen onboarding PR found. ### Testing - Full standard and hosted-authentication mobile suites, static analysis, and required push checks passed. - iPhone preview iterated with Kenny; final simulator walkthrough below uses synthetic data and does not authenticate or save account changes. - Independent review completed; regressions cover cancellation during loading, Add recovery after failed/conflicting memberships, retrying skipped communities, and explicit picker stages. Walkthrough recorded before the copy and recovery followups: https://github.com/user-attachments/assets/bfea3ad3-1dde-40c4-8748-44c84226c292 --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
770ab7ff82 |
perf(mobile): decode relay query responses off the UI isolate (#8669)
### Context Profiling on an iPhone 12 mini (Profile build, real community) showed `RelaySessionNotifier._postForEvents` producing contiguous main-isolate busy runs with p90 110 ms and max 124 ms (27 runs of 50 ms or longer). Stacks were dominated by `jsonDecode` of `response.body`, with `NostrEvent.fromJson` smaller. The whole HTTP response decode ran on the UI isolate: gzip inflate (done by `dart:io`), the UTF-8 decode behind `response.body`, JSON parsing, and event parsing. ### What changes - Successful `/query` and `/profiles/search` responses (the only two callers of `_postForEvents`) are now decoded in a single `Isolate.run` from `response.bodyBytes`: inflate, UTF-8, JSON, validation, `NostrEvent.fromJson`. The parsed events come back by ownership transfer (`Isolate.exit`), not a copy. - Gzip: the query transport now sends `Accept-Encoding: gzip` and sets `autoUncompress = false`, so compressed bodies reach the isolate still compressed. Whether bytes are gzip is decided by the gzip magic bytes, so a transport that already inflated (or an injected test client) still works. Error bodies are inflated inline (small). - Unchanged: `FormatException` messages, the malformed-event wrapping, `RelayException.body`, and the rate-limit gate reading the error body. - Fencing (Review-Proven Rule 2): the `_disposed` / `_ageRestricted` / `_contextGeneration` check still runs before the decode and now runs again after it. If the session retires during the decode, the query reports `StateError` even when the decode itself failed, matching the old ordering where a retired session never decoded. ### Design notes - `Isolate.run` per response instead of a long-lived worker: no lifecycle or disposal to manage, no idle isolate heap on a small phone, easy to fake in tests, and the same shape as `parseProfileEventBatch`. The main-isolate side of a spawn measured at microseconds, so there is no size threshold; small responses take the same path. - Event id and signature verification does not happen on these events on the main isolate today (nothing in the mobile app verifies queried events; NIP-OA verification in profile parsing is already in an isolate), so nothing was moved. - Other HTTP decode sites found and left alone: invite create/join, hosted join policy, BuilderLab account, media upload response, community icon lookup, dev push lease NIP-11, archived-agents. These decode small, rare responses (one small JSON object each), where an isolate adds complexity without a measurable gain. Media and avatar byte downloads do no JSON decoding. The mobile app has no `/count` caller. ### Validation Added tests (`relay_session_test.dart`, through the production `queryRelay` / `searchProfiles` path with the real isolate decoder): parsing with non-ASCII content; profile search; gzip success body and `Accept-Encoding`; gzip error body feeding the rate-limit gate and `RelayException.body`; malformed JSON; non-list body; non-object element; malformed event; invalid UTF-8; error status skips decoding; session disposed, age-restricted, or retired-before-decode while a decode is outstanding. The fencing tests are falsifiable: removing the post-decode check fails the disposed and age-restricted tests; removing the retired-wins-over-decode-error branch fails its test; removing the pre-decode check fails the never-decodes test. `FakeRelay` (test helper) now injects an inline decoder, because `fakeAsync` cannot deliver isolate results; it calls the same parse function. The default transport still honors `http.runWithClient`, which the hosted journey tests use to fake the network (`relay_http_query_client_test.dart`; fails without the fix). `just mobile-check` and `just mobile-test` pass. Host benchmark (AOT executable on an Apple silicon Mac, median of 25, copy of the parse logic, main-isolate stall measured with a 0.5 ms heartbeat timer): | Response | Main-isolate blocked before | after | | --- | --- | --- | | 500 events, ~630 KB | 2.4 ms | 0 ms (max gap 0.05 ms) | | 500 events, ~630 KB gzip | 3.2 ms | 0 ms (max gap 0.19 ms) | | 500 events with 50 20 KB avatars, ~1.6 MB | 5.1 ms | 0 ms (max gap 0.28 ms) | | 500 events, ~4.5 MB | 14.0 ms | 0 ms (max gap 0.27 ms) | | 500 events, ~4.5 MB (827 KB gzip) | 17.4 ms | 0 ms (max gap 0.35 ms) | Total latency is unchanged within noise (for example 14.5 ms to 14.9 ms). The host is much faster than the 12 mini, so absolute device savings should be several times larger. On the iPhone 12 mini (Profile builds of base and this branch, real community over the production relay, two runs per scenario; main-isolate stalls measured with a 4 ms heartbeat timer, since the wireless device tunnel has no VM service): | Scenario | Main-isolate `_postForEvents` time, base | this branch | Max stall, base | this branch | | --- | --- | --- | --- | --- | | Cold start + 60 s (~16k events, 7.8 MB gzip) | 282 to 295 ms (max 116 ms) | 0.01 ms | 147 to 150 ms | 100 to 102 ms | | Inbox + search (~7k events) | 122 to 123 ms (max 111 ms) | 0.01 ms | 138 to 142 ms | 97 to 99 ms | | Open 6 channels (558 events) | 5 to 6 ms | 0.01 ms | 88 ms | 86 to 87 ms | | Background 10 s + resume (~7k events) | 118 to 124 ms | 0.01 ms | n/a (suspension) | n/a | - The production relay serves gzip (about 4.4:1; the largest catch-up response is 3.15 MB on the wire, 13.9 MB inflated, 7k events). Inside the isolate that response takes 30 to 47 ms to inflate, 39 to 55 ms for UTF-8, 65 to 105 ms for `jsonDecode` and 7 to 32 ms for `NostrEvent.fromJson`. - Many 16 to 90 ms stalls remain on both builds; they come from other work (mostly markdown build and layout), not response decoding. - Home, channels, inbox, search, profile and a thread render the same on both builds. buzz-review-completed 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
a7e65c91da |
feat(mobile): choose remote agent models and gate agents on Builderlab sign-in (#8650)
Follow-up to [#8626](https://github.com/block/buzz/pull/8626). Mobile remote-agent creation now matches desktop. - **Model and reasoning effort:** Create agent has Model and Reasoning effort menus. Their options come from the remote service's catalog, and nothing is preselected (same as desktop). The server checks the selection against the catalog again before registering, and saves it on the agent. - **Builderlab gate:** creating a remote agent requires a hosted community and Builderlab sign-in. The Agents directory, community bots and the Bestie chooser stay available without it. Signing out closes an open Create agent sheet; the Agents sheet stays open. - **Enrollment:** `attest-agent` enrolls the agent in the requested community, as on desktop, so the separate `add-buzz-subscription` call is gone. - **Quieter progress:** spinners replace the progress bars and the "Working…" label. Status and success text ("is ready", "Remote agent deleted.", setup steps) is gone, and only failures show text. Refreshing an already-loaded agent list no longer blanks it. - **Shared setting menu:** `BuzzSettingMenu` is a bordered setting row that opens a Material 3 menu spanning the row's width. Opening it dismisses the keyboard. The channel-TTL setting in Create channel uses it too. Agent avatars and QR/link-based creation follow in stacked PRs. Editing existing agents comes later. | Agents | Create | Model menu | | --- | --- | --- | | <img src="https://raw.githubusercontent.com/block/buzz/d0bee87c45d30b9bdd2163a75966ccd5d64f322a/pr-8650--1-agents.png" width="240"> | <img src="https://raw.githubusercontent.com/block/buzz/d0bee87c45d30b9bdd2163a75966ccd5d64f322a/pr-8650--2-create.png" width="240"> | <img src="https://raw.githubusercontent.com/block/buzz/d0bee87c45d30b9bdd2163a75966ccd5d64f322a/pr-8650--3-model-menu.png" width="240"> | ### Validation - Checked on the staging iOS simulator with a Builderlab account: the Agents list, the Create sheet, and the model and effort menus (including with the keyboard open). - Added tests: catalog-gated model selection (including switching models clears an unsupported effort), the Builderlab sign-in gate, controller creation and resume with a model, a failed listing reported instead of an empty registry, and create sheet busy and failure states (including the catalog reload after a rejected create). - The hosted-only agent tests are skipped without Builderlab defines, which `mobile-test` doesn't pass. They pass locally with the staging defines (20/20). - Not covered: Android. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: george <973be878edd6d4f79692a3827f20a6f7fe0ed0362f269ff6b6ba8867033d7d8e@sprout-oss.stage.blox.sqprod.co> |
||
|
|
974495fd4a |
fix(mobile): resume reconnect replay from the last live point and spread reconnects (#8659)
## Summary A relay rolling deploy closes every socket with 1012. On reconnect, mobile replayed each live subscription's **original filter** (startup `since`, full `limit`) unless that subscription had received an event, so quiet subscriptions re-read history back to app start every time. Reconnects used a fixed 1s delay with no jitter. The delay also reset to 1s on every successful connect, so a client bounced from pod to pod during a deploy kept coming back at the same moment as every other client. **This PR makes two changes:** 1. **Resume cursor.** If a subscription reached EOSE on a connection, it records `liveUntil`: the time of the last frame received from the relay on that connection, capped at the clock at close. The replay then uses `since = max(original since, lastSeen − 5s, liveUntil − 960s)`, and never goes past the filter's `until`. - **Why 960s:** the relay accepts `created_at` up to ±900s from its clock (`MAX_TIMESTAMP_DRIFT_SECS` in `crates/buzz-relay/src/handlers/ingest.rs`). An event stored after the close can therefore be backdated by that much, and the extra 60s covers client clock skew. - **Only relay frames count as liveness.** Close and error paths never extend `liveUntil`, so a close noticed after the device sleeps doesn't overstate how long the subscription was live. - **Ambiguous EOSE is ignored.** If a subscription has a second REQ on the same socket, that subscription doesn't advance `liveUntil` for the rest of the socket. A CLOSED ends liveness for the rest of that socket, because the retry is a second REQ. Subscription IDs are never reused, so an old EOSE can't mark a re-subscribe as live. - **No cursor:** a subscription that never reached EOSE keeps its previous cursor, or replays as before. 2. **Reconnect spreading.** - After a 1012 or 1001 close, the first attempt waits a uniform random delay in [0, 10s]. A client already backing off waits the longer of that delay and its backoff. - Ordinary backoff gets symmetric ±25% jitter, capped at 30s. - The backoff resets only after a connection stays up for 60s, not on connect. This matches desktop. - Foreground resume and a manual `reconnect()` still reset to the base delay, as before. **Simpler?** The pure reconnect math (cursor, jitter, spread, stability) lives in a new `relay_reconnect_policy.dart` and is table-tested on its own. `relay_session.dart` was near its 1200-line cap, so its five small private bookkeeping classes moved verbatim to `relay_session_subscriptions.dart`. Net concept count is unchanged. **Identified debt (not changed here):** - Dart's WebSocket ping/pong isn't visible to the session, so a long-idle but healthy connection that drops replays back to its last application frame − 960s. This is still bounded, and much cheaper than replaying from app start. - The unread catch-up in `channels_provider.dart` runs one REQ per active channel (`since = readAt + 1`, `limit: 1000`) on reconnect and every 60s. - A retryable CLOSED is retried with a 30s cap, no jitter and no attempt limit. - `lastSeenCreatedAt` advances on stored events received before EOSE. If a relay returns stored events newest-first and the socket dies before EOSE, the next replay can skip older stored events. - The `lastSeen − 5s` term keeps its existing gap for events backdated after a close. ### Related issue N/A. This is part of a set of reconnect-load reductions. Companion PRs: relay read-pool warm (#8651), desktop (#8652), agent harness (#8653). ### Testing - Mobile gate on a Linux workstation, at the head SHA: - `dart format --set-exit-if-changed`: pass. - `flutter analyze`: no issues. - Gateway recipe tests: pass. - `flutter test`: 3881 pass, 1 fail. `push_gateway_origin_validator_test` fails the same way on `main` on that host, due to a host git-config error. - New `relay_reconnect_resume_test.dart`: - pure table tests for the cursor and the delay - a real-socket test that reads the 1012 close code - journeys through `RelaySessionNotifier`. They check that a close or error after an hour asleep doesn't extend liveness, that a second REQ or a delayed EOSE for an unsubscribed ID can't advance the cursor, and that a backdated event inside the 960s window is still replayed. - 22 production mutations, each failing the new tests. They cover every cursor term, the 960s window, the `until` clamp, the liveness cap, the ambiguous-EOSE guard, CLOSED handling, jitter, the 1012 spread and floor, the backoff reset, and close-code capture. - `fake_relay.dart` now draws a fixed jitter midpoint, so existing journey tests stay deterministic. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Luke Tornquist <tornquist@squareup.com> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
d6196a16c1 | fix(relay): revoke invitations and restrict claims (#8237) | ||
|
|
5a8102ee5b |
fix(mobile): preserve invite onboarding and offer identity setup (#8617)
## Summary Cold-start iOS invite links could open the welcome screen without continuing sign-in, and accounts without a stored backup had no explicit identity setup choice. Forward scene launch URLs to `app_links`, offer creation for an unbound account with no backups, and offer matching-key import for an existing binding without a backup. Existing key storage, binding, backup, and admission checks remain in use. Discover saved hosted destinations using `known-communities/list` and remember successfully admitted destinations with `known-communities/add`. Saved destinations are not membership proof; normal admission and the existing failed-claim membership reconciliation remain separate. A failed save preserves the intent and local community for idempotent retry. ### Related issue Related to #8599 (QR-first mobile onboarding) and #8601 (invite policy consent; overlapping setup UI). No exact duplicate found in the open onboarding PR / identity-backup issue search. ### Validation before the latest main integration - 48 tests passed across five focused files: `hosted_journeys_test`, `hosted_communities_test`, `hosted_communities_flow_test`, `hosted_environment_flow_test`, and `hosted_multi_backup_recovery_test`. Test defines enable hosted domains and the push gateway. - Four additional pairing handoff tests passed with hosted domains enabled after updating their shared HTTP fixture for community saving. - Targeted Dart analysis: no issues. Required pre-commit and pre-push hooks passed, including the broader mobile analysis/test lane and file-size checks. - Native iOS build passed during iteration. On an isolated iOS simulator, a terminated app opened from an invite automatically entered browser login; human recording confirmed login continued into identity setup and new identity creation. - Human staging testing on October 27 passed invite entry with an existing email/password and new identity, community entry, messaging, cold restart, and recovery on two separate clean iOS simulators. These results apply to build `ff2138fc325790e0af4d9a64b2ed033fae3a8295`; the backend backup-upload blocker described previously is no longer blocking that tested flow. - Fresh-account signup through an invite is deferred. Physical-camera QR scanning, native regression/mutation coverage, independent review, and native retesting after the latest main integration remain outstanding. - Known limitation: failed-claim membership reconciliation still calls `communities/joined`; its endpoint/response contract needs alignment with the supported backend before that fallback is considered ready. The successful primary recovery path does not prove this fallback. - No account reset tooling, environment-specific credentials, generated dependency files, or recording artifacts are included. ### Current review fixes and validation Head `9902a98d42904a4a7d44a79541d81ceb94d8eac3` addresses the two review blockers and both small import races: - Hosted imports explain a linked-account key mismatch; the shared self-hosted/occupied-device message remains unchanged. The linked-account journey enters the wrong key through the actual sheet, checks the visible error and empty storage, then imports the correct key. - Closing the import sheet while its operation is pending now waits for that operation and resumes successful setup. The new-account journey covers creation and import, including dismissal while awaiting the status response, binding, backup receipt, and retry. - An unbound account with newly appeared backups refuses direct import, preserving recovery's receipt-before-adoption rule. The existing multiple-backup invariant covers matching and different imported keys. - The configured route journey uses host labels and its existing fake account now supports destination saving; the real-account journeys separately exercise persistence. 53 tests passed across six focused files with CI's `BUZZ_AUTH_DOMAINS=buzz.xyz,builderlab.xyz,builderlabstaging.xyz` and push-gateway define. In a separate clone with independent Git metadata/build output, restoring the old production import paths failed the three intended regressions; restoring the fixes passed all 43 tests in that control. Required pre-commit and pre-push hooks passed, including mobile analysis/tests and file-size checks. Hosted CI and native retesting of this head are still pending. The earlier independent review was at `10815e5d9`; these fixes still need reviewer confirmation. Optional accessibility and import-navigation refinements are deferred. Fresh signup, authenticated production add/list and the membership fallback remain open as described above. ### Staging testing and production configuration Staging selection is build configuration, not a changed production default. `HostedEnvironment` is unchanged by this PR: `BUZZ_BUILDERLAB_URL` defaults to `https://app.builderlab.xyz`; `BUZZ_AUTH_DOMAINS` is empty by default (hosted account features disabled). Builds that enable production hosted domains receive these onboarding behavior fixes after merge. This PR does not deploy anything or redirect production builds to staging. Check out this PR (`gh pr checkout 8617` in a dedicated `block/buzz` checkout/worktree), install dependencies per `mobile/README.md`, and boot a clean iOS simulator. With the repository-pinned Flutter toolchain: ```sh . ./bin/activate-hermit ./scripts/mobile-worktree-overrides.sh cd mobile flutter run -d <clean-simulator-udid> \ --dart-define=BUZZ_BUILDERLAB_URL=https://app.builderlabstaging.xyz \ --dart-define=BUZZ_AUTH_DOMAINS=communities.public.builderlabstaging.xyz \ --dart-define=BUZZ_DEBUG_PRIVATE_LOGIN=true ``` Private login requests an ephemeral browser session only in debug builds. Hosted relay assertions remain off by default; this test does not validate assertion-enabled admission. Generated local Xcode settings, dependency metadata, credentials, and simulator state are not included in the PR. - Invite test: open a valid public-staging invite in the simulator browser, choose **Accept invite in Buzz**, sign in or sign up, complete identity setup, enter the community, and send a message. - Recovery test: preserve that simulator, start another clean simulator with the same build, choose **or sign in with email**, and sign into the same account. Verify the recovered public key matches, the saved community and earlier message return, and a new message sends. - Fresh signup: repeat invite entry using a new account. This still needs human verification. Keep this PR draft while the listed validation gaps remain. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> |
||
|
|
d4a07063c8 |
feat(mobile): manage Builderlab remote agents (#8626)
Adds Builderlab remote agents to mobile: a member signed in to Builderlab can list, create and delete their own hosted agents from the channels header, and active remote agents show as Online. Everything is gated by `HostedEnvironment.enabled` (the build-time `BUZZ_AUTH_DOMAINS` define). App Store and internal release builds do not set it, so they render and fetch nothing new. ### Behavior - **Agents** button in the channels header (native iOS bar and Material header) for hosted communities only. It opens a sheet listing the community's agents; tapping one pushes its profile inside the same sheet. - **Create**: for the member's own agents (the NIP-OA owner is the member, not the community). Registers the agent with Beekeeper, attests it to the community, saves optional private instructions, subscribes it to the relay, then publishes the kind 30177 registration with a NIP-OA owner proof. A failed step resumes with the same agent identity on retry; instructions stay in secure storage, never in preferences. - **Delete**: removes the relay registration, confirms it is gone, then deletes the Beekeeper agent. A partial failure keeps a retry row in the list. - **Presence**: an ACTIVE agent with a live kind 30177 registration on the active relay shows Online in profiles, channel rows, DMs and mentions, since remote agents wake on messages. Local presence data is unchanged. - Sign-in reuses the shared hosted flow. The account's saved Buzz key must match the server binding; nothing is adopted or rewritten. - `BuilderlabAccountClient` gains an allowlist for the six Beekeeper routes desktop already uses. Not included from the prototype: the separate entrypoint and second sign-in, test-community creation, staging-only hosts, QR-code agent links, and the USB proxy. ### Validation - Added tests: build-time gate, account/key binding before listing, setup resume with private instructions, deletion contract and retry, owner-proof verification, status parsing, presented presence requiring a live registration in the active community and following registration publish and removal (mutation-checked), account-change clearing, load retry, recovery of a pending setup whose instructions were lost, a lost registration response that can only resume with the same idempotency key (from any community), an HTTP-rejected registration that can be discarded, deletion of other agents while a setup is unfinished, rejection of owner proofs whose signed conditions or profile signature fail, registration publish and removal over a loopback WebSocket relay for an `http://`-saved community, and widget tests for in-sheet detail navigation, creation overlay with failure then resume, and delete confirmation. - The new tests also run in the CI lane with `BUZZ_AUTH_DOMAINS` configured, so the enabled path is covered. - Full mobile format, analyze and test suite pass locally. - Independent agent review: no gate leak or key-handling issue found; its robustness findings (discarding a setup that never got a server identity, clearing registry state on account change, retrying a skipped load, guarded notifications) are fixed; discard and account change have added tests. - Simulator: a `BUZZ_AUTH_DOMAINS` debug build launches to Builderlab sign-in. The signed-in flow was not re-run on this branch. - Codex Security Review: an earlier head received a published review, and its two findings were fixed. On |
||
|
|
d090ac24bb |
perf(mobile): cut idle and resume sync churn (#8490)
## Summary Mobile does background work nobody sees, and it waits on round trips it doesn't need. This PR fixes four things: - **Chats you've left let go.** A chat with no listener for 30 s drops its live and typing subscriptions. Coming back shows the cached timeline right away, then resubscribes and catches up. - **No requests before AUTH.** The relay rejects any REQ that arrives before NIP-42 AUTH completes. History requests are now held until the socket authenticates, and they flush after the rate-limit gate. Live subscriptions skip their REQ, and the replay on connect sends it. `auth-required` is terminal for a live subscription, so a pre-AUTH live REQ could be dropped for good. - **Resume reconciles the channel list once.** `resumed` is published before the session hears about it. So the channel list now decides one microtask later: if the session is reconnecting, the connect does the reconcile. - **The 60 s backstop only runs in the foreground on a connected session.** After an OS suspension, the missed tick fires at unfreeze, about 450 ms before the app hears `resumed`, while the session still looks connected. Without this, that tick triggered a second full sync. Scope note: while the app is in the foreground, the backstop still re-downloads membership every 60 s. That matches desktop, which refetches every 60 s while focused. To drop it, mobile would need to subscribe to the live membership notices (44100/44101), as desktop does. That would be a follow-up. ### Related issue N/A. This is follow-up work from the mobile perf dive. None found. ## Before and after Simulator setup: iPhone 18 Pro simulator, debug build, a local relay behind a proxy that adds ~120 ms RTT, and a seeded account with 120 channels. I scripted one identical run against each build: 1. Open 5 channels. 2. Visit 20 more channels. 3. Wait 40 s. 4. Stay idle in the foreground for 3 min. 5. Go to the background for 3 min. 6. Resume. `main` is `ca3be5501`; the branch is `d77c4e86f`. The last amend changed only test names. Since then, chat history waits for the live EOSE again (see the note below), so channel open is back to `main`'s order. | | `main` | this PR | |---|---|---| | Live subscriptions after visiting 20 chats | 61 | 11 | | Resume: membership reads | 2 (244 kB) | 1 (122 kB) | | Resume: websocket REQs | 36 | 31 | | Resume: REQs sent before AUTH | 9 | 0 | | Resume: REQs rejected as `auth-required` | 2 | 0 | | Idle foreground, 3 min | 3 membership reads, 12 REQs | same | Notes: - **Channel open:** an earlier revision requested history without waiting for EOSE, which cut the median open from 297 ms to 197 ms. Review showed it can lose a message. The relay registers the live REQ only after its access checks, so a message committed in between reached neither the window read nor the subscription. A timestamp overlap only covers messages signed within it. Keeping that speedup safely would mean a second window read on every open, so it's left out. - **Idle foreground is unchanged by design.** See the scope note above. - **Background traffic:** the simulator suspends the app shortly after it leaves the foreground, so both builds sent almost nothing during the 3 minutes in the background. The backstop fix shows up at unfreeze instead, as the missing second membership read. - **Resume inbound events:** in this one run, events received over the websocket on resume were 247 kB on `main` and 338 kB on the branch. I haven't attributed the difference. ## Test These extend the existing journeys, plus one new journey: - **`channel_messages_journey_test`:** - The window read waits for the live REQ's admission, and a message signed a minute earlier but committed before admission shows up. - After the listener leaves, the chat keeps following for 29 s and stops at 31 s. - Coming back shows the cached rows immediately, then catches up on a message sent while away. - **`relay_session_test`:** a stale-connected resume sends nothing before AUTH. Then exactly one history REQ and one live REQ go out after AUTH. - **New `channels_sync_journey_test`:** drives the real lifecycle, session and channel notifier against `FakeRelay`: - A backgrounding just before a backstop tick sends nothing. - A resume after the session paused syncs once. - An OS-suspension resume with AUTH held syncs once, after AUTH, and sends no REQ before it. - **`FakeRelay`:** gained `holdReq`, `holdAuth`, `before_id` pagination and an injectable clock. Each guard was mutation-checked, and every mutation failed its test: - live REQ sent before AUTH - history sent before AUTH - resume refresh not deferred - backstop without the lifecycle check - history read alongside the live REQ - a 30-day detach delay Follow-up, not in this PR: a history REQ already in flight on a socket that gets replaced still waits for its 8 s timeout. Princess Donut found this in review; it predates this change. ## Validation - `flutter analyze`: clean - Full mobile suite at `678617b`: 3,696 passed - Court review (Princess Donut on the relay session, Mongo on chat subscriptions, Mordecai on the channel list plus a challenge pass): no blocking findings. The rate-limit flush ordering came from that review. **Human test:** Wes ran `da5077d` on the simulator with his real account and confirmed it works. `460dc35` then fixed a review blocker: a chat rebuilt while detached stayed detached. A retest of that path is pending: leave a chat for more than 30 s, background the app for more than 5 s, resume, then reopen the chat. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f7708d0694 |
Polish mobile onboarding and profile setup (#8438)
Originally stacked on #8375, now merged. Rebased onto main. Polishes the onboarding landing page, community-link entry, and recovery sheets. Profile setup uses a consistent sheet with a larger avatar, inline display name, and draft photo/emoji/animated editing; iOS uses native controls and coordinated avatar/sheet transitions. Validation: mobile analysis, full tests, and normal pre-push checks passed after rebase. Prior iOS build and device walkthrough passed; native validation was not repeated after this rebase. Snapshots omitted as requested. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> Co-authored-by: Tom Brow <tomb@block.xyz> |
||
|
|
edbfbf08db |
test(mobile): journey-first testing guidance and suite cleanup (#8397)
## Summary This PR does two things. It updates the mobile testing conventions to favor a few journey tests over many small logic tests. It also applies those conventions to the existing mobile suite, one commit per step. ### Testing conventions (`AGENTS.md`, `mobile/README.md`) - Start with one widget journey per user-visible flow, driven through the real widgets, notifiers and coordinators. Assert what the user sees and what is durably saved. - Cover retry, cancel and resume as steps inside the journey, not as separate tests. - Fake only external boundaries (network, relay, OS, browser, storage). - Use focused, table-driven tests only for security and data-loss invariants, protocol or crypto vectors, and pure functions with many input combinations. - When review finds a defect, extend an existing journey or table instead of adding a test per finding. - Delete a lower-level test only once a journey demonstrably catches the same failures. - Every test must fail when the production code it guards is broken. These conventions keep the Review-Proven Rules' safety bar and change only how coverage is organized. ### Suite inventory and plan The suite has 234 Dart test files, about 110k lines and about 2,600 declared tests. Native tests are separate: BuzzPushKit, RunnerTests, NativeControlTests and Android. Line-saving estimates are hypotheses, not a deletion budget. Each deletion needs a replacement that rejects a representative production mutation. 1. **CI and reliability.** Run the native tests CI skipped, make time-dependent tests deterministic, and make tests that could not fail falsifiable. *(done, see below)* 2. **Shared harness.** Add one fake relay (socket plus HTTP boundary, with the real `RelaySessionNotifier`) in `test/helpers/`. Today 38 files each write their own `RelaySessionNotifier` fake. *(done, see below)* 3. **Per-area journeys.** Convert page tests that fake the flow's own notifiers (for example, `channel_detail_page_test` overrides about 60 providers). Fold near-duplicates into tables. Remove constant-pinning, pixel-geometry and call-count tests once a journey covers the outcome. Split `channel_detail_page_test.dart` (17.7k lines). Each area commit reports before/after counts and the mutation it rejects. 4. **CI speed.** Shard by file instead of by test, since every shard currently compiles the whole suite. *(done, see below)* Keep as-is: relay protocol and NIP handling, crypto vectors, auth, push security, persistence and journal invariants, recipient routing, and real-socket transport and cancellation tests. ### Step 1 changes - **Android CI** runs the 23 JVM unit tests, which were never run before. It retries the APK build once, only on SDK or plugin download failures (the cause of 4 recent `main` failures). - **iOS CI** runs all RunnerTests (previously 2 of 99) and the previously uncalled Me-agent accessibility script. The Swift Native lane builds the simulator app once, in the background while the simulator boots and runs the native-control scripts, instead of twice. The lane stays non-required. - **Stale RunnerTests.** Two reaction-details tests still expected the filter list that #8280 replaced; they now cover the toggle list. #8393 moved navigation bar controls onto a shared glass surface and made unknown Tabler symbols assert. The avatar and members-activity tests now find controls on that surface, three tests use real symbol names, and a 240 pt title width pinned to the old control sizes is removed (its gap assertions remain). - **Known bug, not fixed here.** Since #8393 the DM title avatar image is nil, because `makeItem` returns avatar data as a custom-view item. No screen passes a title avatar yet. The test marks it `XCTExpectFailure`. - **Typing retry** and **sections backoff** tests use fake time instead of about 37 s of real sleeps and a `Stopwatch < 100ms` threshold. - **Manage-channel sheet tests** previously asserted inside callbacks that the sheet's `catch (_)` swallows. A wrong saved description made the test loop until killed. The tests now record arguments, assert outside the caught code, and cancel instead of re-presenting endlessly. - **`video_viewer_transport_test` removed.** It exercised only `package:http`. Removing the production `request.sink.close()` already fails `video_viewer_test`. ### Step 2 changes - **`test/helpers/fake_relay.dart`** fakes only the websocket (through the session's socket factory) and the HTTP bridge (through its client). The real `RelaySessionNotifier` still connects from `build()`, handles OK and CLOSED, batches events, backs off and replays subscriptions. - The fake serves the NIP-CW channel window. It can hold OK and live echo independently, reject a chosen publish, and drop the connection. Any request it cannot answer faithfully fails the test at teardown, because the app often catches relay errors and falls back. - **`channel_messages_journey_test`**: history through the channel window, a live message, a send that is pending until OK and stays one row after the echo, a rejected send rolled back, then drop and reconnect with missed and later messages. - **`channel_send_journey_test`**: type and send on `ChannelDetailPage` with no feature-notifier overrides. The row and sending spinner show at once, OK settles them, and a rejected send puts the text back in the composer and shows a snackbar. - Existing per-file relay fakes and `SidebarRelay` are unchanged. Step 3 migrates them area by area. ### Step 4 changes - `scripts/mobile-test-shard.sh` splits the 235 test files across the 3 shards, balanced by line count. `flutter test --total-shards` split tests, so each shard compiled and loaded every file. Locally at 4 workers, one shard took 63 s instead of 73 s and used 40% less CPU. `channel_detail_page_test.dart` (17.7k lines) is now the long pole; splitting it in step 3 shortens the slowest shard. Out of scope here: requiring branches to be up to date before merge (a repository setting), and the onboarding tests in #8375. ## Validation - Mutation checks: - Typing retry bound: test fails. - Sections backoff reset: test fails. - Wrong saved description: fails in about 11 s. The old tests hung instead. - Removed sink close: `video_viewer_test` fails. - Changed Dart files pass locally, and `flutter analyze` is clean. - Android `testDebugUnitTest` passes locally (23 tests). The Me-agent accessibility script passes on a local simulator. - Step 2 mutation checks, each made a journey fail: accepted OK ignored, rejected row not rolled back, optimistic row not added, draft not restored, local message left pending after OK, channel window replaced by the fallback, live subscriptions not replayed after reconnect. - Both journeys run in about 1 s on fake time. - All 99 RunnerTests pass on an iPhone 16 iOS 26.5 simulator, the CI destination, after rebasing on #8398. - The shard script partitions all 235 test files exactly once. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
dcced2c4f7 |
fix(mobile): preserve saved identity lookup after enumeration fails (#8470)
An account-scoped saved identity could fail to open when secure-storage enumeration succeeded while showing choices, then failed before selection. Retain the discovered storage location and reread/validate that exact record before falling back to inventory lookup. Only public location hints are retained; signing material is read fresh. Follow-up accepted at merge of #8375: https://github.com/block/buzz/pull/8375#pullrequestreview-5577985447. Validation: existing self-hosted widget journey now injects enumeration failure after choices load and verifies the same key is admitted and persisted. Three focused files passed: 37 tests with hosted domains configured; 28 passed and 9 hosted-only skips with default configuration. Changed-file analysis passed. In an isolated clone, removing the location lookup failed that journey; control and restored runs passed. Self-review found no blocking defect. Draft acceptance gate: native/device behavior and human acceptance are not established by widget fixtures. On a disposable saved identity, enter a self-hosted community link, choose the identity, and confirm admission preserves it; cancel/retry as well. Hosted live-backend acceptance remains separate and pending backend availability. No full local suite was manually run; mandatory hooks/CI provide broader coverage. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@feedback.communities.buzz.xyz> |
||
|
|
b6aa5539e3 |
feat(mobile): account onboarding, key recovery and hosted reconnect (#8375)
## Behavior Adds optional mobile Builderlab sign-in, identity backup/recovery, community discovery/creation, and account-aware reconnect. **Builderlab features are disabled by default:** when `BUZZ_AUTH_DOMAINS` is unset or empty, account login, creation, backup/recovery, reconnect prompts, and background account restoration are hidden/disabled. QR/code pairing, links, and invites retain Nostr-only entry. Saved account state is retained for later opt-in. `--dart-define=BUZZ_AUTH_DOMAINS=communities.buzz.xyz,communities.public.builderlabstaging.xyz` opts those DNS domains and their subdomains into required account setup. The same policy gates pairing, deep links, durable community admission, and reconnect. A nonempty list enables Builderlab features; `BUZZ_BUILDERLAB_URL` alone does not. Enabled flows still require their backend services. Existing Nostr relay authentication remains mandatory. `BUZZ_HOSTED_RELAY_AUTH=true` enables scoped relay assertions and reconnect prompts only for configured authentication domains. Configure assertion issuance and each community's issuer binding before enabling assertions; enable relay enforcement last. Account credentials stay at Builderlab. Wrong-account recovery preserves keys; failed writes are not replayed; authentication is bounded by request deadlines. Hosted flows preserve keys across cancellation/restart, reconcile interrupted creation/invitation acceptance, and support manual nsec file import/export. Account identities are scoped to backend and authenticated subject; reusing an existing local key for an unbound account requires an explicit selection. Recovery reconciles the advertised and recovered inventories before automatic selection. Successful self-hosted admission retires its pending key, and iOS startup removes interrupted export files. Adds authenticated relay `GET /operator/communities/joined` for the companion Builderlab membership endpoint. ## Validation - Follow-up review repair at `66a12ac92`: self-hosted entry presents readable choices and import despite incomplete enumeration, while generation still requires a complete empty inventory; explicit selection uses exact-key lookup. Consuming a value-equal duplicate now advances dispatch. Reconnect requires an existing account binding or a subject-scoped backup receipt before signing an assertion, and pins the subject inside the serialized account request. An empty different account is refused without sending key proof. Focused checks: configured domains 58 passed; default 49 passed, 9 hosted-only skips. Separate isolated mutations fail the incomplete-inventory sheet, exact-key reuse, duplicate dispatch and reconnect journeys (including an empty-account-only bypass); restored controls pass. Evidence uses synthetic storage/network boundaries, not physical Android or live backend association. All mandatory commit/push hooks passed; fresh GitHub CI is pending. - Bounded review repair at `d3cc0003f`: incomplete secure-storage enumeration now blocks automatic identity generation while explicit known-key reads retain their fallback; declined/failed onboarding links release the queue; HTTP 429/5xx keeps Retry regardless of response encoding. Default app-gate coverage exercises a bind link followed by ordinary community entry. Focused checks: configured domains 47 passed; default 38 passed, 9 hosted-only skips. Removing the three production fixes in an isolated source/Git/build copy fails the identity-resolution, declined/failed queue, and Retry journey assertions; clean and restored controls pass. Storage and HTTP failures are synthetic; physical Android and live outage behavior were not exercised. Published after all mandatory commit/push hooks passed (mobile analysis/tests, branch-skew and file-size gates); fresh GitHub CI remains pending. - Existing whole-PR source review cleared the reconnect defects at `fb15d5b82`. - Domain-policy increment: focused environment, entry, app-gate, pairing and onboarding journeys pass with empty and configured domains. Existing pairing provider/page tests pass. Changed Dart files analyze cleanly. - Isolated source/Git/build mutation: forcing paired credentials through hosted login fails the default-build journey; clean and restored controls pass. CI now runs configured-domain journeys in addition to the default-build suite. - Integrated main through `5b433d929` at `0067c069c`, including its pinned Flutter upgrade. Preserved the PR welcome UI with the updated transition API and added Material surfaces around onboarding selection/consent rows. Focused post-merge runs pass in both configurations; all mandatory local push gates passed (mobile analysis/tests, Rust tests, desktop-native checks, branch-skew and file-size checks). - Merge-review repair at `a315bce11`: focused identity/storage/admission and widget journeys pass, including account switching with an empty or already-bound second account, explicit reuse, and self-hosted removal/rejoin with another key. Default-domain journeys pass too (hosted-only cases intentionally skip there). - Isolated source/Git/build mutation checks: bypassing account scoping, reuse consent, pending-key retirement, or recovery inventory reconciliation fails the relevant journey/invariant assertions; clean and restored controls pass. - Native `RunnerTests/IdentityFilePluginTests`: both tests pass on the iPhone 18 Pro iOS 27 simulator with an isolated app identifier. At `24a0dcaac`, the picker journey seeds residue in Library/Caches, matching path_provider_foundation, and verifies plugin startup removes it while preserving an unrelated file. The earlier tmp-only fixture did not exercise the actual export location. This is synthetic native cleanup/picker evidence, not a real terminated-share or live account round trip. - Follow-up: Caches and tmp cleanup; fallback to individually known identity slots when Android bulk enumeration fails; explicit replacement of a refused self-hosted key preserves its original before swapping the retry slot. Configured-domain journeys/invariants: 34 passed; default: 31 passed, 3 hosted-only skips. Isolated enumeration/retry mutations fail with passing controls. Bulk-read fallback preserves but cannot enumerate arbitrary other-account slots until bulk reads recover. - Configured-domain CI exposed a stale pairing-status assertion after account scoping. The repaired test checks the admission mismatch, preserved account/transfer keys, and no admitted community. The configured-domain CI selection passes locally (56 tests); removing the production mismatch guard fails the updated test, with clean/restored controls passing. This is synthetic coordinator evidence, not physical QR acceptance. - Final review repair (`40e1e6101`): native iOS export creation uses atomic complete file protection; normal and restart cleanup remain. Account/policy/claim HTTP uses one 30-second total deadline with request abort and body cancellation. Removed the unrelated NIP-FA spec edit. - Focused account/admission/export tests: 25 pass; configured-domain account/admission/onboarding journeys: 33 pass. Isolated deadline and native-dispatch mutations fail, with passing clean/restored controls. Native export/picker tests: 2 pass on iOS 27 simulator. The real file-protection attribute assertion is physical-device-only because simulator provides no Data Protection attribute; locked-device protection and interrupted real sharing have not been observed. - Opt-in UI: configured-domain CI selection passes (57 tests); default selection passes (7 tests, 10 hosted-only skips); URL-only plus whitespace/empty domains passes (6 tests, 9 hosted-only skips). Changed Dart files analyze cleanly. Independent mutations that unconditionally initialize the coordinator in the app gate, welcome entry, or link form fail the default journey; clean/restored controls pass. - Native welcome screens exercised on iPhone 18 Pro / iOS 27 simulator in default and configured builds: default exposes community link / QR / pairing code only; configured build additionally exposes Builderlab connection and community creation. No live hosted login was exercised in this check. - Delivery at `e48fe73a9`: all mandatory push hooks passed (full mobile analysis/tests, Rust tests, desktop-native checks, branch and file-size checks). GitHub checks had not yet populated at the immediate post-push snapshot. ## Review disposition | Feedback | Disposition | | --- | --- | | Explicit self-hosted entry under unreadable inventory, duplicate invite dispatch, reconnect cross-account binding ([entry](https://github.com/block/buzz/pull/8375#discussion_r4318421622), [queue](https://github.com/block/buzz/pull/8375#discussion_r4318421640), [reconnect](https://github.com/block/buzz/pull/8375#discussion_r4318455512)) | Repaired at `66a12ac92`; evidence above. Reconnect refuses unproven ownership rather than silently establishing a binding. | | Incomplete Android identity inventory, stranded deep-link queue, transient HTTP retry classification ([review](https://github.com/block/buzz/pull/8375#pullrequestreview-5576864143)) | All three repaired in `d3cc0003f`; focused journeys and isolated mutation evidence above. | | Account cross-link, incomplete recovery inventory, committed invite checkpoint, pending local key retirement/replacement | Repaired in the preceding commits; journey and mutation evidence above. | | iOS interrupted key export ([review](https://github.com/block/buzz/pull/8375#pullrequestreview-5575253252)) | Native cleanup plus complete protection at creation implemented. Physical-device lock and real interrupted sharing acceptance remains pending. | | Trickle responses bypass idle timeout; unrelated NIP-FA edit ([review](https://github.com/block/buzz/pull/8375#pullrequestreview-5575764556)) | Total request deadline/abort implemented; spec edit removed. | | Production domains rejected | Ordinary entry now needs no Builderlab configuration. Opt-in account-required domains must still use supported community mappings. | | Lost Android Keystore recovery | Backup exclusions prevent the reviewed stale-restore scenario; genuine key loss still has no in-app recovery/reset and remains deferred. | | Native secp256k1 dependency question | Obsolete against the current base: upstream C integration landed in #8398; no package diff remains here. | | Operator membership endpoint contract | Documentation now states the operator chooses the pubkey; no end-user session binding is implied. Additional handler-level coverage is optional follow-up. | | Optional Android partial document cleanup/activity retention; local work after backup cancellation | Follow-up hardening, not claimed implemented. Cancellation prevents remote backup upload. | | Semgrep crypto inventory notices | Informational inventory, not demonstrated defects. | ## Remaining draft gates Native QR/browser-return acceptance remains unverified for this revision. Optional Builderlab onboarding still needs backend backup-status and joined-community routes, recovery policy/configuration, and real backup/recovery/reconnect acceptance. The empty-domain build removes these services as prerequisites for ordinary pairing; it does not make the optional hosted flows backend-ready. Human acceptance and current CI are still required before merge. Review dispositions retained: production auth-domain configuration must use supported community mappings; enabling `builderlab.xyz` does not add support for `buzz.block.builderlab.xyz`. Recovery after genuinely losing an Android Keystore key remains a follow-up, not a newly implemented capability. Human acceptance steps: build once without `BUZZ_AUTH_DOMAINS` (including with a previously saved account) and confirm no Builderlab controls/restoration while ordinary link and QR/code entry work. Build with supported domains and confirm connection/creation appear; exercise browser return and live backup/recovery/reconnect. On physical iOS, exercise export and interrupted sharing/locked-device protection. Record results before marking review complete. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@wes.communities.public.builderlabstaging.xyz> |
||
|
|
8222e99915 |
fix(mobile): keep thread references consistent across push and Inbox (#8332)
Follow-up to [#8294](https://github.com/block/buzz/pull/8294). Malformed trailing thread tags could make a notification open the right thread while Flutter indexed its reply elsewhere; uppercase references could similarly miss lowercase event IDs. Share one validated thread-reference parser between the mobile timeline, Inbox, and feed helper. It ignores malformed markers, keeps the last valid root/reply, and normalizes lookup IDs to lowercase. Native push parsing now uses the same casing. Existing thread fixtures use valid event IDs so they continue exercising real thread relationships. Add RunnerTests coverage for the production native-to-Flutter argument conversion, including buffered cold-start navigation and omitted optional thread IDs, and run it in the native CI lane. ### Validation - New parser and Inbox regressions cover malformed markers and uppercase references. Widget regressions open and highlight the exact reply for valid, uppercase, and trailing-malformed tags. - After integrating [#8106](https://github.com/block/buzz/pull/8106), Flutter analysis and the complete mobile suite pass: 3,337 tests, four existing skips. Integration preserves per-event read predicates and migrates incoming thread fixtures to valid IDs. An unchanged keyboard-history test also failed with main's original parser; recording reads before the scroll transition fixes its timing assumption while retaining visibility assertions. - Native package tests passed (127 tests), and both Runner bridge tests passed on an iOS simulator with Xcode 26.5. Removing the production `threadRootId` assignment made the bridge regression fail; restoring it passed. Native code is unchanged by the main integration. - [Current-head CI passed](https://github.com/block/buzz/actions/runs/38659307247), including Mobile and Mobile Swift. [Codex review](https://github.com/block/buzz/pull/8332#issuecomment-6181458528) and [security review](https://github.com/block/buzz/pull/8332#issuecomment-6181449235) found no issues on the integrated commit. [Owner approval](https://github.com/block/buzz/pull/8332#pullrequestreview-5526812957) covers the exact integrated commit. - Standards and spec reviews found no blockers, including the merge resolutions and test adjustments. Validation is scoped to mobile/native per VISION_MOBILE; unrelated repository-wide checks were stopped, not counted as passing. The bridge test covers serialization and buffering, not end-to-end APNs delivery or app launch. No new physical-device acceptance is claimed; routine post-merge dogfood remains human-owned. --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
1ac0a1271b |
fix(mobile): preserve Markdown attachment destinations (#8168)
## Summary Fix mobile “Image unavailable” errors caused by angle-bracketed Markdown URLs. Preserve attachment destinations across images, galleries, videos, and file links, including spaces, parentheses, and optional titles. ### Related issue Related: #8086 (attachment filenames). ### Testing - `just mobile-install mobile-check mobile-test` - Regression coverage decodes PNG/JPEG/GIF/WebP/BMP and verifies gallery viewers, video routing, and authenticated file links. - Native device playback not tested. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
f7ff4294fa |
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 <pfleger.will@gmail.com> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
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> |
||
|
|
4fc720cc0d |
feat(mobile): frosted glass navbar + channel detail polish (#404)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
a444fb9516 |
feat(mobile): scaffold Flutter app with Riverpod & Catppuccin theme (#306)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |