mirror of
https://github.com/block/buzz.git
synced 2026-10-29 13:31:44 +00:00
remote-prototype-current
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6286a54321 |
feat(relay): store per-community NIP-FI issuer bindings (#8372)
Stack: this PR → #8377 → #8399 → #8401 bb-public creates communities all the time, so a hand-edited `BUZZ_NIP_FI_COMMUNITIES` map can't list them. This PR stores an explicit NIP-FI issuer allowlist for each community in the database and writes it when the community is created. Login checks don't read the table yet; that comes in #8377 together with the database-backed resolver. Operator steps are in [`docs/operator-nip-fi-bindings.md`](docs/operator-nip-fi-bindings.md). **Storage.** Migration `0066_community_nip_fi_issuers` adds `community_nip_fi_issuers (community_id, issuer, created_at)`, with primary key `(community_id, issuer)` and the community write fence attached. The assertion audience is `https://<communities.host>`, so the table has no `aud` column. Zero rows means no binding. The table is included in whole-community deletion and purge. **One bindable rule.** `BUZZ_NIP_FI_PROVISION_BINDING` (`{"host_suffix": ..., "authorized_issuers": [...]}`) is parsed in `buzz-auth` with `BUZZ_NIP_FI_ISSUERS`, whatever `BUZZ_NIP_FI_MODE` is set to, and both the relay and `buzz-admin` use the same parser. Each authorized issuer must be a configured issuer (1 to 2048 bytes), and the suffix must not cover any host declared in `BUZZ_NIP_FI_COMMUNITIES`; an unreadable declaration refuses startup even in mode `off`. `ProvisionBindingPolicy::is_bindable(host)` is true for a host exactly one label under the suffix that is not reserved. It decides creation-time binding, explicit `bind`, and what the lookup route reports. **Binding on creation.** Rows are written in the same transaction as the community insert on all three creation paths (`POST /operator/communities` `create_only`, its legacy convergence branch, and startup bootstrap), and only when the call actually created the community, so existing rows are never rewritten. **Reserved host.** A host whose `https://<host>` equals a configured `command_audiences` value never holds a binding. Operator provisioning of it is rejected with a 400 and the `reserved_host` error code, bootstrap creates it unbound, and the availability route reports it unavailable with an error code. `main.rs` calls `verify_reserved_hosts_unbound` at startup; if a reserved host has rows the relay refuses to start and names the fix (`buzz-admin nip-fi unbind --host <h> --issuer <i>`, or revert the command audience). A database failure during that check is a separate `Check` error. **Operator commands.** - `buzz-admin nip-fi backfill` pages unbound communities by host (1000 per page) and binds each page with one `INSERT ... SELECT` limited to active communities. It skips communities being deleted, always prints `{rows_bound, unbound, unbound_hosts, truncated}`, and exits non-zero while any bindable community is still unbound. Archived communities are bound on purpose. Both bind paths take the shared deletion lock and then lock the community row (`FOR NO KEY UPDATE`, in id order for a batch), the same order deletion uses, and the backfill insert also requires the community to still have no binding, so an operator `bind` that lands after the backfill listed a community wins and the default issuer is not added beside it. - `buzz-admin nip-fi bind --host --issuer` refuses a reserved host, a host outside the suffix, or an unconfigured issuer. - `buzz-admin nip-fi unbind --host --issuer` refuses to remove a community's last issuer (archive or delete the community instead), with one exception: when a valid provisioning policy shows that backfill can't bind the host (it's reserved, or outside the suffix), the last issuer can be removed. That is the reserved-host recovery path in the runbook. A missing or malformed policy doesn't block `unbind`; it just means the exception never applies. **Lookup route.** `GET /operator/communities/nip-fi-binding?host=` (NIP-98, `RELAY_OPERATOR_PUBKEYS`) reads the database and returns `{host, active, issuers[]}`. `host` is the stored host. It returns 404 for an unknown host, 200 with `active:false` for an archived or quiescing community, and `issuers:[]` when no provisioning policy is set. It lists only issuers that are configured and on a bindable host. kgoose depends on this contract; it is also documented in the runbook. **NIP-FA.** The `relay_url` bullet now also says: "An adapter MAY obtain its exact relay list from a configured, authenticated source; it MUST still compare `relay_url` exactly." **Deployment dependency.** The kgoose config side is already handled by [cash-server#137671](https://github.com/squareup/cash-server/pull/137671). In public-community mode it requires the command audience not to be a community audience. --------- 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> |
||
|
|
2f79374f48 |
feat(relay): channel labels (#8175)
🤖 ## Summary A plugin or agent often needs to tag its own channels and then find them again, for example "my attention-config channel" or "all channels of this workspace". Today a channel has no labels: the relay ignores any `t` tags in create or edit requests. This PR adds channel labels. - A create request can carry up to 8 labels as `["t", "<label>"]` tags. - An edit request with `t` tags replaces the label set. `["t", ""]` alone clears it, the same convention as `["ttl", ""]`. Only owners and admins can change labels, as for the name. - The relay publishes each label as a `t` tag on the channel's state events, after the channel type tag. Part 2 of 3, stacked on part 1. ### Related issue None found. ### Details - **Rules.** A label uses 1–64 characters from `a-z`, `0-9`, `.`, `:` and `-`. The relay refuses a label equal to a channel type name (`stream`, `forum`, `dm`, `workflow`, `system`), so a label cannot make a channel look like another type. - **No trust from labels.** Any admin can set a label. Only the relay-written creator tag (part 1) says who made a channel. - **Storage.** New migration `0061_channel_labels.sql` adds `channels.labels TEXT[] NOT NULL DEFAULT '{}'`. Channels without labels look and behave as before. - **Readers.** The CLI and `buzz-acp` read the channel type from the first `t` tag. Before this PR they used the last one, which would now be a label. - **Spec.** NIP-CL gets a "Channel labels" section with these rules. ### Testing - Relay tests: labels at creation appear after the type tag, edit replaces and clears them, invalid labels are refused, and members who are not admins cannot change them. - Unit tests for the label rules: allowed characters, length, reserved type names, the 8-label cap and the clear marker. - `buzz-acp` test: the channel type comes from the first `t` tag, not a label. ### Stack These are two stacks. Merge them in this order. Client support (creating, labeling and finding these channels) will be in block/buzz-app, not the buzz SDK and CLI. **Stack 1: channel labels and creator lookup (#8174–#8176)** 1. #8174 channel type and creator tags, and the channel creation time 2. #8175 channel labels ← this PR 3. #8176 database-side label and creator match, and the rule for choosing one channel **Stack 2: system channels (#8180–#8182)**, stacked on stack 1 because it uses labels and the creator tag 4. #8180 the system channel type 5. #8181 leave system channels out of reads that do not name them 6. #8182 channel-state filters opt in to system channels --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
868e2f8f32 |
refactor(push): one app profile per gateway (#8206)
Buzz push currently selects an application profile, such as `dogfood`, during client registration. We want each gateway to serve one Apple app configured by the operator instead. This removes profile selection from the client, relay, and gateway, while allowing internal and public gateways to use their own Apple identities. This PR implements that model in the Buzz source and upstream deployment chart. Discovery, enrollment, grants, and leases no longer carry an application-profile selector. Each gateway has one configured Apple application and APNs transport. The protocol changes are incompatible with the old registration format, so relays advertise `buzz-push-v1` instead of `nip-pl`. Old clients do not discover the new push capability. Updated clients use separate Keychain records and register again. The [push notes](https://github.com/block/buzz/blob/push-capability-v1/docs/buzz-push.md) describe the experimental contract. Landing this PR is the first step. Publish the matching gateway image and upstream chart, then land the [shared chart update](https://github.com/squareup/cash-helm-charts/pull/5740) and [gateway deployment update](https://github.com/squareup/builderbot-platform-core-infrastructure/pull/1085). Before cutover, stop old gateway instances and check legacy registration state. Migrations refuse to proceed if old gateway installations or active relay leases remain; they do not delete that state. Physical-device verification remains necessary before production activation. ### Validation Gateway and focused Flutter push tests pass, as do the Swift package, simulator build, and native notification/enrichment tests. A local relay advertises `buzz-push-v1` without `nip-pl` or `app_profiles`. The PostgreSQL suite passed 868 tests; its two migration-test failures were corrected and pass a focused rerun together with the gateway cutover guard. Chart rendering and release-contract checks pass. Desktop JavaScript tests, desktop and web builds, native desktop compilation, mobile analysis/tests, and desktop native tests pass. The full Rust unit lane and prescribed `just test` command pass, including workspace integration tests against task-isolated local services. The production overlay now requires an explicitly supplied APNs topic, with omission and rendered-value regression coverage. The corrected Swift checksum assertion passes the complete package suite: 78 XCTest tests and 42 Swift Testing tests. No physical-device enrollment or production cutover is claimed. Local merge validation: the embedded migration test and formatting pass after reserving profile migration 0060 behind rollback migration 0059. The local pre-push Rust lane failed ACP timing/steering tests (`idle_resets_on_stdout_activity`, `keepalive_resets_idle_past_deadline`, and the failed/missing-outcome steering tests). ACP and its dependency manifests are unchanged by this branch; all four pass in an isolated single-thread rerun. The documented local-check exception skips only the Rust pre-push lane for publication; required CI remains mandatory. Mobile and desktop pre-push lanes passed. Combined migration validation: after rollback 0059 landed, the main merge preserves profile migration 0060 and both embedded migration assertions. The focused migration test and formatting pass. The next local push again failed only the unchanged ACP idle/keepalive timing tests (1053 passed, 2 failed, 3 ignored); other lanes passed. Publication uses the same documented local Rust-lane exception; required CI remains mandatory. Follow-up review fix: removed the legacy profile column from the copied-lease fixture. All four event-follow-up contract tests pass on isolated PostgreSQL databases, covering desired and migrated schemas in dual and app-only modes. Formatting passes. Publication retains the prior passing local-lane evidence and uses the authorized pre-push exception for the known unrelated ACP timing failures; required CI remains mandatory. --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
5b907b838a |
fix(push): isolate push enqueue and stop workers on rollback (#8216)
Push enqueueing previously ran inside the message transaction, so a push lock or database error could block or reject a message. Configured relay writers now submit bounded best-effort work only after commit. A dedicated connection and bounded queue isolate enqueue waits and failures from message-write connections. Overload, errors, process loss, or shutdown may drop a notification; the committed message remains authoritative. `BUZZ_PUSH_ENABLED=false` disables the application producer and workers. Updated writer connections always bypass any surviving enqueue trigger. The overlap migration updates an existing function without resurrecting retired objects. Apply it before relying on isolation with a surviving trigger, and replace every old enabled process and writer connection: missing settings preserve legacy behavior during overlap. SIGTERM/Ctrl-C cancels enqueue, matcher, and delivery work before listener drain. Durable queues, claims, leases, preferences, and delivery configuration remain intact. Claims expire normally; gateway requests already accepted may still deliver. Reactivation requires an explicit operator decision about retained work. There is no automatic purge, startup interlock, or backfill of messages written while disabled. Built on merged [#8135](https://github.com/block/buzz/pull/8135), including [#8178](https://github.com/block/buzz/pull/8178). Trigger retirement [#8210](https://github.com/block/buzz/pull/8210) remains open; this change is tested with the trigger present and after removal. Operational details are in [the rollback and reactivation guide](https://github.com/block/buzz/blob/relay-push-rollback/docs/push-gateway-deployment.md#relay-rollback-and-reactivation). ### Validation [ |
||
|
|
1e80066c93 |
feat(relay): reshape read-state API to the sidebar wire format (#8220)
## Summary Changes the private read-state API (`/buzz/v1`) to the sidebar wire format agreed in #buzzin-in-secret. **The counting code is unchanged.** Requests and responses take the new shape, so the client integration can build against it now and later behavior fixes (counting rules, followed threads, another-device pings) ship without another wire change. Nothing ships against v1 yet (no buzz-app use on `main`), so this replaces version 1 directly. Supersedes #8188 (same branch content, rebased onto current `main`, plus the thread-cap and doc changes below). ``` GET /buzz/v1/me/sidebar?limit=&cursor= | ?channel_ids=a,b,… → { channels: [channel], next_cursor } channel: { channel_id, unread: bool, mentions, read_through_id|null, latest_id|null, threads: [thread] } thread: { root_id, unread: bool, mentions, read_through_id|null, latest_id } POST /buzz/v1/me/read-state { intents: [{ type: "mark_through", target: { channel_id [, root_id] }, message_id }] } → { outcomes: [applied|blocked|invalid|unknown], channels: [channel] } ``` What changed and why: - **Channel and thread positions are fully separate.** Channel `unread`/`mentions` count only top-level messages. Thread replies count only on their thread row. `mark_channel_read` and its whole-channel cut are gone. "Mark channel read" sends a channel `mark_through` at `latest_id`, plus one per thread row to clear threads too. - **Channel `latest_id` is always a top-level message.** It used to be able to point at a reply, and a channel `mark_through` rejects a reply as its anchor. It also skips a reply-marked message whose ancestry never resolved, because marking through one would fail on every retry. - **`read_through_id`:** frontiers stored only the anchor's arrival time. Migration 0058 adds `personal_read_frontiers.through_message_id`, which follows the greatest arrival, and drops the now-unused `threads_through_timestamp`. This is the only storage change. - **Counts are plain numbers.** The exact/at_least/unknown wrappers are gone. Evidence the relay can't use (unusable tags, unresolved ancestry, undecided conversation membership, past the scan cap) leaves the message out, so counts can undercount. - **POST returns the updated row** of each distinct channel it applied to, so the client doesn't refetch after every read. If those rows can't be read after the writes commit, `channels` is omitted and the outcomes still stand. The row refresh shares the 8 s intent deadline. - **Up to 25 thread rows per channel** (was 5). Five hid a mention in a sixth unread thread. 25 is the most that keeps a POST's rows for `MAX_INTENTS` (100) channels inside the 1 MiB response limit; 50 would be ~1.4 MB, a 503 that hides already-committed outcomes. A unit test builds that worst-case response and fails if the cap outgrows the limit. - **The doc separates shape from behavior.** `/buzz/v1` promises a stable shape (fields, types, status variants, what `mark_through` does). What the counts include (which messages are unread or directed, which threads are listed, the bounds) may change while v1 is pre-release, so clients replace their rows with each response instead of reconciling against local counts. - **Removed from the wire:** the explicit-contexts read (`GET /me/read-state?targets=`), `account`, channel name/type/archived/hidden (these come from Nostr metadata), `latest_message_at`/`_complete`, and the NIP-11 `max_contexts`/`max_context_messages`/`max_thread_summaries`. Deliberately deferred (behavior or additive, no shape change): - Thread counting rule (relay counts a reply if you wrote or replied to its direct parent; the client uses "posted anywhere in the thread or followed"), the workflow-owner attribution p-tag, and thread-row `mentions` counting every counted reply rather than directed ones. - `follow`/`unfollow` intents, listing followed threads that are already read, and a truncation flag for the thread list. Thread: buzz://channel/6f773b94-34d0-4004-bec9-a100b6ad17d5 (#buzz-bff, root 31020fff) ### Related issue Follows #8157. Supersedes #8188; replaces the wire parts of the closed #8155. ### Testing - `cargo test --no-fail-fast -p buzz-db -p buzz-relay` at |
||
|
|
7afdc2aac9 |
feat(relay): record when a read-state account starts (#8157)
## Goal Add `started_at` to read-state accounts from the day the table is created. This prevents users seeing ALL their channels as unread when they first install a client update that starts using these APIs --------- Signed-off-by: hMB2 <0400eb0da32afbbf16cdd1dee6ffd243587e8f3d6a2823fe7dfa95d16b002b7d@buzz.block.builderlab.xyz> Signed-off-by: ART <ea39c1c8acf57180161ef4ba4ed68abf8589f5291956b909a5aa991fc66664ca@buzz.block.builderlab.xyz> Signed-off-by: hMurderbot <219c58afff8ac2c76e4e527a329aa61c2ebbb896786b1916d4f9cca5140ecd04@buzz.block.builderlab.xyz> Co-authored-by: hMB2 <0400eb0da32afbbf16cdd1dee6ffd243587e8f3d6a2823fe7dfa95d16b002b7d@buzz.block.builderlab.xyz> Co-authored-by: hMurderbot <219c58afff8ac2c76e4e527a329aa61c2ebbb896786b1916d4f9cca5140ecd04@buzz.block.builderlab.xyz> Co-authored-by: ART <ea39c1c8acf57180161ef4ba4ed68abf8589f5291956b909a5aa991fc66664ca@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
745501962e | refactor(schema): fan desired-state schema out to one file per table (#8131) |