## Summary Removed private-channel members can currently download attachments if they know the URL. With `BUZZ_MEDIA_READ_MODE=enforce`, fresh GET, HEAD and Range requests check current membership and readable references on the primary database. Removed members receive 404; remaining members retain access, and rejoining restores access. Thumbnail aliases use the parent attachment’s permissions. Publication checks also prevent copied private URLs from granting new access. They cover attachments, link previews, profile pictures, banners, group pictures and emoji. Persona creation and changed-avatar edits, plus team and individual snapshot imports, transfer animated posters and animations under the agent’s signer before profile publication. Failed persona transfers preserve existing local records; import failures appear in the results. Authorization uses bounded queries, a two-second statement deadline and transaction-local JIT suppression. An index on edit targets avoids broad scans. Publications have a 512-reference limit, and capacity waits up to 500 ms before refusal. Protection defaults off. Existing downloaded bytes cannot be recalled. All modes stop creating immutable HTTP cache entries. ### Related issue Related approaches: #7565, #5185, #8451 and #8444. This change evaluates live stored references directly. ### Testing Published head: `3d1aa7431cc495a001a0f13e95e2e422c5a77ab1`, rebased on main `2cd40badb85de059a6916206e133da7a0a88fb2e`. Push hooks were **SKIPPED** with explicit author approval. Current-head HTTP, full package, PostgreSQL migration/media and profile-filter mutation checks are pending; CI is pending. The earlier complete HTTP journey passed at `f20b449262` (identical source tree), with its evidence attributed separately. Earlier failing preview/report-fixture runs are preserved; they do not count as passes. The HTTP journey covers upload/publication, fresh GET/HEAD/Range revocation and rejoin, emoji replacement, edited previews, persona visibility and non-reference decoys. The migration regression forces an index timeout, verifies no 0086 history or media tables, then proves a migrator through the base's 0085 can restart. Human attachment removal/rejoin testing passed on the pinned `1f08a8e0fd2edf19dc94321f7258518149fe1bf6` trial. Native animated persona edits and team/individual import acceptance remain incomplete. That old-snapshot database stays intact and cannot run the reconciled binary; a new trial needs a separate fresh database. ### Rollout prerequisites - Build and verify the reference index before shadow mode. Small databases can auto-migrate through one atomic 0086 migration within the configured timeouts. It builds the index before creating the media tables, so failure records no new migration version. Large deployments must disable auto-migration and prebuild indexes concurrently. - **Enforce remains blocked unless the operator can rule out SDK/CLI attachment edits and the unverified agent-harness edit producer.** SDK/CLI edits still omit attachment metadata. That behavior is deferred, not fixed. Removing this blocker requires latest-effective attachment metadata across successive edits, no restoration of removed files, and an upload → successive edits → GET/HEAD regression. - Validate client signer compatibility and split emoji publications above 512 references before enforce mode. - Complete the human workflows above in an isolated community with enforce enabled. - Follow the [index deployment guide](https://github.com/block/buzz/blob/honor/private-channel-media/docs/private-media-index-deployment.md) and [operator guidance](https://github.com/block/buzz/blob/honor/private-channel-media/docs/operator-private-media.md) for rollout and rollback. The [prebuild timeout finding](https://github.com/block/buzz/pull/9077#discussion_r4486221478) and [avatar-transfer finding](https://github.com/block/buzz/pull/9077#discussion_r4486221458) are implemented and independently reviewed. ### Schema change checklist - [x] Additive expand and code deployment; no backfill or contract phase. Prebuild/verify before shadow under either schema deployment path. - [x] No SQL function, trigger, procedure, rule or foreign key added. Signed events are untouched. - [x] No migration mutates existing rows. - [x] Migration and desired-state schema match. Catalog parity, target parsing, future partition inheritance and upgrade refusal are covered. - [x] Old/new binaries coexist for serving. Suspend community deletion during expansion and while rolled back to a binary with the old deletion catalog. - [x] Keep auto-migration disabled on old binaries after schema expansion; sqlx rejects unknown versions. - [x] Migration lock timeout 1 s; statement timeout 5 s. Small populated databases build inline within those budgets; large deployments use concurrent child-index prebuild/attachment with auto-migration off. Missing, invalid or wrong indexes fail verification. Generated with Codex --------- Signed-off-by: Brad Seiler <seiler@squareup.com> Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
13 KiB
Private-channel media revocation
BUZZ_MEDIA_READ_MODE is a startup setting: off (default), shadow, or
enforce. Unknown values fail startup. Channel revocation is effective only
in enforce. Blossom authentication, tenant binding, and community admission
remain mandatory in every mode.
Read policy
Every GET and HEAD, including each Range request, authorizes the parent hash before returning blob bytes, thumbnail bytes, existence, size, or range errors. A tenant sidecar lookup precedes the new database lookup. Authorization uses a fresh primary-database repeatable-read snapshot, without membership caches or replicas. A removal committed before that snapshot excludes the member. A request authorized before removal may finish afterward.
A file is readable through any current readable reference. Shared bytes may therefore remain readable through another open channel or a DM the person still belongs to. Channel owners/admins have no blanket exception to private channel membership. Ordinary REQ event visibility, recipient-gated kinds and encrypted engrams retain their restrictions. Gift-wrap ciphertext itself cannot supply hidden attachment references.
Counted references are imeta url and image fields, link-preview snapshot image/favicon fields, profile picture (or image when
picture is absent) and banner fields, group metadata picture tags, and emoji tags on emoji lists
and sets and per-emoji entries (kind 30180). Persona definitions (kind 30175)
count avatar_url, with the same owner-only/shared visibility as their events.
Root-relative paths and the tenant's HTTP/HTTPS media bases count.
Animated profile descriptors count their poster and decoded animation URL
independently, with the same tenant check on each. The indexed profile lookup
expects hash characters to remain literal, as in the desktop descriptor.
Republish over-encoded hashes such as %61 or JSON Unicode-escaped hash characters such as \u0061 using canonical URLs. Ordinary Unicode escapes in profile names are supported; the URL hash must remain plain ASCII. A pasted URL in free text alone grants nothing; messages must publish imeta.
Deleted events/channels grant nothing. Only the newest live edit supplies a
message's attachments; its original must remain live. Original link-preview
snapshots remain readable through an edit unless the original or any live edit carries the exact
["link-preview","none"] marker; later edits do not undo suppression; this does not retain removed attachments.
Superseded profiles
are excluded. Current community icons grant community-wide access.
The read path probes the existing tags GIN index for imeta hashes and exact local URL aliases. It scans content only for current kind-0 profiles and kind-30175 persona avatar fields in the community, never all message bodies. LLVM/JIT is disabled only in this short authorization transaction: compiling partitioned permission probes costs seconds without amortizing that cost. Other database work retains its configured JIT policy. A two-second statement deadline and a 1,024-candidate budget per probe and a 16 MiB aggregate work budget bound each lookup. An accessible candidate can succeed before an overflow refusal. Candidates stream one at a time rather than allocating an entire page of message bodies. Budget exhaustion or a database failure returns 503; an inaccessible/missing reference returns 404. NIP-FI strict mode keeps its existing authorization-unavailable response contract.
Uploads and publication
After the upload pipeline verifies the supplied bytes, the relay records
media_uploaders through community write admission. This is independent of
optional audit/upload moderation records. An unposted file is previewable by
its uploader only. Real reuploads may prove possession for another uploader.
Possession authorizes publication; a known URL/hash alone does not. A registered
agent must hold its own upload proof or readable reference; owner proof is never inherited.
The shared publication check runs before common ingestion dispatch (WS and
HTTP, including metadata commands) and before direct workflow persistence.
It uses the same reference extractor as reads, plus structured command URLs.
A durable media_publications marker consumes preview authority before
publication dispatch and survives source deletion. A later rejected operation
may conservatively consume a preview; reupload still proves publication
rights. Reuploads also record a boundary when a legacy reference already exists, so later purging that reference cannot revive previews. The marker grants no download access. Both tables are purged during
community deletion, and every writer uses admission.
Deployment-admin feedback attachment reads retain their independent admin and feedback-row provenance checks. Feedback does not grant community-wide media access through the ordinary media endpoint.
Rollout and recovery
- Before any pod enters shadow, build and verify the edit-target index. Small databases may auto-migrate through 0086: a missing index builds within a one-second lock and five-second statement budget, even with data. Large deployments disable auto-migration and prebuild concurrently per partition before applying 0086 or desired-state schema. A budget failure leaves migration retryable; a valid prebuild skips the blocking build. Bypassing the schema step can cause lookup timeouts/503 in enforce. Restore off, finish index construction/verification, then begin shadow. The migrations add plain tenant tables and an index, with no SQL functions, triggers, foreign keys or data rewrite.
- Deploy the new binary on every writer with mode
off. - Restart all writers in
shadow; inspectbuzz_media_read_access_total{mode="shadow",reason="no_ref"},reason="not_member", andreason="lookup_error", plusbuzz_media_publication_access_total. Exercise avatars, icons, emoji, uploads, private messages and old attachments during a staging soak. - Repair unsupported/historical URLs or republish them through an authorized
uploader before restarting every serving pod in
enforce.
Enforce blocker: SDK/CLI attachment edits remain unsupported.
buzz_sdk::build_edit and buzz messages edit currently omit attachment imeta.
Editing an attachment message through these clients can make its file unreadable,
including to its sender. This review item is deferred, not fixed. Enforce stays blocked unless the operator can rule out SDK/CLI attachment edits
in the community. The agent harness edit producer is unverified and remains
inside this blocker until its metadata behavior is traced and tested. To remove
the blocker, update both SDK/CLI paths to preserve the
latest effective attachment metadata across successive edits without restoring
files removed by an earlier edit. Require an upload → successive edits → GET/HEAD
regression proving retained files stay readable and removed files stay denied.
Keeping a URL in the edited body does not preserve access.
There is no grant backfill: existing stored references are evaluated directly, and signed event tags are never rewritten. Legacy unreferenced/orphan blobs have no upload proof and are denied in enforce mode; their owner must reupload. Pre-enforcement references remain authoritative, including a preexisting avatar that points to the same bytes as a private attachment. Custom historical media hosts beyond the tenant's current authority require explicit repair.
Roll back policy by restarting with off; retain the additive schema and
publication markers. If rolling back to a pre-0086 binary, keep auto-migration
disabled and suspend community deletion: its exact-set deletion catalog does
not recognize the two added media tables, so deletion fails closed. Resume
deletion after deploying a binary whose deletion catalog includes them. Mixed off/enforce serving pods cannot provide revocation.
Shadow/off do not close the original exposure. Shadow authorization lookup failures, including sidecar failures, are counted
without refusing reads. The serving path can still fail if storage cannot supply
bytes. Enforcement fails closed.
Successful media responses, authorized range errors and media denials use
private, no-store in all modes, including off. Off preserves the previous
authorization behavior; cache headers intentionally change before enforce to
avoid creating more year-long immutable entries during the preparatory rollout. New
responses therefore increase repeat-download bandwidth. The desktop proxy
passes this policy to the WebView; its local file cache and the mobile client's
local copies can still display previously fetched bytes. Old immutable HTTP
cache entries, files already downloaded, screenshots and copied bytes cannot
be recalled. The guarantee applies to requests reaching the relay; migrating
existing client caches is a separate client rollout concern.
Any authorized member may deliberately republish or reupload bytes into a readable channel or avatar. Revoking one channel cannot revoke that independent share. This policy does not prevent authorized recipients from copying data.
Media authorization uses a shared semaphore across database-handle clones,
limited to one quarter of the writer pool, capped at eight lookups. Saturation
waits up to 500 ms before returning authorization-unavailable, counted with
reason="capacity", before pool acquisition. Every Range
request reauthorizes; all events, including emoji lists and sets, have a 512-reference cap, and checks run
sequentially within that connection bound. Missing sidecars reject publication
in enforce mode; shadow counts failures and accepts the write, including stale
avatar metadata edits and upload-proof/publication-marker errors.
Eight slots provide an upper bound of 8 / mean_lookup_seconds lookups per
second per pod before queueing (for example, 20 ms means about 400/s; 100 ms
means about 80/s). A smaller writer pool reduces that bound proportionally.
These are capacity calculations, not measured throughput targets. Measure full
request latency and concurrent image/avatar bursts, including all probes,
sidecars and pool acquisition. Keep sustained traffic below the measured bound;
the 500 ms queue absorbs bursts but cannot increase throughput. Each lookup
uses a writer connection and several statements; no-store repeats that work on
every view. The synthetic query-plan times exclude storage/network/pool costs.
During staging soak, abort enforcement if writer authorization acquire p95 exceeds 50 ms for five minutes, event-write acquire p95 doubles its pre-rollout baseline, or media lookup_error/capacity refusal exceeds 0.1% of requests for five minutes. Investigate query latency, pool utilization and storage errors before retrying. Shadow failures must be repaired before enforce, since a failed recording does not establish possession. Uploads made in off mode record no proof: uploads not yet posted when enforce starts require reuploading.
Before enforce, every client that uploads avatars with one key and publishes
profiles with another must transfer the actual poster/animation bytes to an
upload signed by the profile key. Owner relationships never inherit possession.
Validate this in each supported client during shadow; upgrading only this
repository's desktop does not migrate other clients. This includes the new
desktop/web app in block/buzz-app, mobile, and CLI/plugin integrations that
publish agent profiles: verify their signer and upload flow before enabling
enforce for their users.
The legacy desktop transfers both poster and animation bytes for changed persona avatars before saving the edit; transfer failures leave the old local avatar intact. Team and individual agent snapshot imports also transfer selected target-local avatars before publishing each new agent profile. Imports report transfer failures in their profile-sync results. Validate all three workflows against enforce mode before rollout. Unchanged-avatar reconciliation never borrows the owner's signer.
New managed-agent creation explicitly downloads the chosen target-local avatar with the captured owner's signer, verifies its image bytes and hash, then uploads those bytes with the new agent's signer. The agent holds its own possession proof. The captured community stays pinned through profile publication, even if the owner changes workspaces during transfer. Automatic reconciliation never borrows the owner's proof or signer.
Changed-avatar edits refuse the entire save before any transfer when a linked instance has no usable signing key. The error names affected instances; restore their keys, or remove/unlink those instances, then retry. The previous persona and instance avatars remain unchanged, so the same requested avatar is retryable. Keys may be permanently lost; waiting alone does not repair a lost key.