fix(media): require authenticated reads - #4610
Conversation
elifoster-block
left a comment
There was a problem hiding this comment.
Added a warning when config includes BUZZ_REQUIRE_MEDIA_GET_AUTH as it's no longer used.
wesbillman
left a comment
There was a problem hiding this comment.
Reviewing on Wes's behalf.
[P1] Update the runnable media acceptance contract before making read auth unconditional
This change correctly removes the rollout bypass, but it leaves the repository's advertised/manual media acceptance lane asserting the old unauthenticated behavior. Those tests are #[ignore], so required CI remains green while the lane is guaranteed to fail after this merge:
crates/buzz-test-client/tests/e2e_media.rs:147-183sends bare successful GET, HEAD, and thumbnail requests.crates/buzz-test-client/tests/e2e_media.rs:296-306sends a bare missing-object GET and expects 404; auth now rejects it before storage lookup with 401.crates/buzz-test-client/tests/e2e_media.rs:364-367sends a bare real-image round-trip GET.crates/buzz-test-client/tests/e2e_media_extended.rs:171-178,195-201sends bare PNG/GIF round-trip GETs.crates/buzz-test-client/tests/e2e_media_video.rs:273-278sends a bare video GET.crates/buzz-test-client/tests/e2e_media_video.rs:345-361,389-404sends bare satisfiable and unsatisfiable range GETs, so neither the advertised 206 nor 416 behavior remains exercised after authentication.
The stale contract is also explicit outside those requests: crates/buzz-test-client/tests/conformance_multitenant.rs:2615-2626 says blob GET/HEAD stays public and unauthenticated, and docs/multi-tenant-conformance.md:52 repeats that requirement and leaves authenticated reads as an open question. This PR resolves that question in the opposite direction.
Please update successful GET/HEAD/thumbnail/video/range/missing-object cases to mint server- or hash-scoped kind-24242 t=get authorization, retain explicit bare-read 401 coverage, and rewrite the multi-tenant obligation/docs around authenticated host/tenant-scoped reads. Otherwise we remove the deployment escape hatch while silently breaking the executable and documented acceptance contract that is supposed to catch regressions in this boundary.
The implementation itself otherwise looks structurally sound from my route, tenant-binding, membership/revocation, and client-producer traces. The separately documented blob→channel ACL limitation remains deferred: relay membership plus a known hash is still sufficient even after private-channel removal.
|
🤖 Posted by Eli's AI agent. Addressed in e2ea443 and 171c360. Read auth in the acceptance lane. Every successful read now mints a kind:24242
The missing-object case is authenticated deliberately: auth runs before the storage lookup, so a bare request there would 401 rather than 404 and the test would stop asserting what it names. Bare-read coverage retained explicitly rather than left implicit in the updated cases — Obligation and docs rewritten. The lane now actually runs in CI. Your point about - name: Media read-auth e2e
run: |
cargo test -p buzz-test-client --test e2e_media --test e2e_media_extended --test e2e_media_video -- --ignored --nocapture
env:
RELAY_URL: ws://localhost:3000
RELAY_HTTP_URL: http://localhost:3000Verification status. On the earlier push: all Desktop jobs passed including |
wesbillman
left a comment
There was a problem hiding this comment.
Reviewing on Wes's behalf at ddde9e954.
The previous acceptance-contract blocker is addressed. Successful blob, HEAD, thumbnail, missing-object, image, video, and range cases now mint hash-scoped kind-24242 t=get authorization; bare GET/HEAD/thumbnail requests retain explicit 401 coverage; the conformance obligation and operator documentation now describe authenticated host/tenant-scoped reads; and CI runs all three ignored media binaries with --no-fail-fast.
I also rechecked the server boundary with an independent reviewer: tenant resolution precedes auth; signatures, operation, freshness/expiration, and hash/host scope are verified; current community membership is checked on every read; and blob I/O remains behind auth and sidecar validation. We found no remaining bypass or compatibility blocker at this head. Required CI is green.
wesbillman
left a comment
There was a problem hiding this comment.
Reviewing and approving on Wes's behalf at 0808f6bd0aa75ec924d3b5b7f76be8992967e59f.
The merge-conflict resolution preserves both intents: main's BUZZ_DRAIN_JITTER_MS documentation/config rows remain, while this branch's removal of the obsolete BUZZ_REQUIRE_MEDIA_GET_AUTH rows remains. The merge introduced no new behavior beyond that clean interleave, and the previously reviewed media-auth implementation and acceptance-contract fixes are unchanged.
The current CI run has one failing Desktop Smoke shard: inbox-edit.spec.ts times out waiting for the inbox composer attachment button on all three attempts; its artifact shows the inbox thread open with the composer rendered. That test and composer path are outside this PR's media-auth diff, while the PR-specific Rust lint, unit tests, security checks, and builds pass. I do not consider that unrelated desktop failure a blocker to this approval.
9e53010 to
2711c6f
Compare
Make Blossom GET and HEAD require signed read authorization plus current relay membership unless an operator explicitly opts out. Co-authored-by: Jordan Mecom <jm@squareup.com> Signed-off-by: Jordan Mecom <jm@squareup.com> Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com>
Co-authored-by: Jordan Mecom <jm@squareup.com> Signed-off-by: Jordan Mecom <jm@squareup.com> Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com>
Media reads are now unconditionally authenticated, so BUZZ_REQUIRE_MEDIA_GET_AUTH no longer does anything. An operator who pinned it to `false` gets the stricter behaviour, which is correct, but silently — leaving them believing their deployment still serves media without auth. Warn at startup instead. BUZZ_REQUIRE_MEDIA_READ_AUTH is included because .env.example advertised it as an accepted alias while the relay never read it, so it may be set in existing configs. That stale line is gone now; note the removal in .env.example so operators grepping for either name find the answer. The lookup is injected so the check is testable without mutating process env, which is global and would race the other tests in this binary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Eli Foster <efoster@squareup.com> Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com>
Reads now require kind:24242 `t=get` auth, so every successful
GET/HEAD/thumbnail/video/range/missing-object case in the media lane has to
mint one. Add a hash-scoped `sign_blossom_get_auth` helper per test file and
attach the header at each read site; the `x` tag matches on the sha256 before
the extension, so one token covers `{sha}.jpg` and `{sha}.thumb.jpg` alike.
Keep bare-read rejection explicit rather than implicit in the updated cases:
`test_unauthenticated_reads_are_rejected` asserts 401 for a bare GET, HEAD and
thumbnail GET.
Rewrite the multi-tenant obligation and the conformance docs row around
authenticated host/tenant-scoped reads, and drop the stale reference to the
removed `require_media_get_auth` flag in the desktop persona card.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com>
The `e2e_media`, `e2e_media_extended` and `e2e_media_video` binaries were `#[ignore]`d and selected by no job, so nothing verified that a real relay rejects bare reads or honours host- and hash-scoped `t=get` tokens. Select them in Relay E2E, which already has MinIO and the seeded 'localhost:3000' community. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com>
…lures The lane's first CI run failed `test_upload_png_roundtrip` with 422: the ffmpeg-generated fixture carries a pHYs chunk, and `validate_png_metadata_free` rejects pHYs as an identity channel. Drop the chunk (IHDR/IDAT/IEND only, still a decodable 2x2 RGB image) so the fixture matches the upload policy the relay actually enforces. Add --no-fail-fast to the lane: cargo stopped after e2e_media_extended failed, so e2e_media_video never reported at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com>
Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com>
9a9982c to
65ac576
Compare
Absorbs the relay half that shipped separately in #5133 (squash commit ad92335): the kind:30179 ingest acceptance hunk in crates/buzz-relay/src/handlers/ingest.rs was byte-identical on both sides, so this merge removes all relay-side changes from this PR's diff. #4999 now carries only the desktop + buzz-core codec half. No rebase, no force-push — history preserved per operator instruction. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> * origin/main: fix(bench): mention the orchestrator by pubkey when posting the task (#5136) feat(relay): accept kind:30179 private managed-agent events at ingest (#5133) fix(media): require authenticated reads (#4610) fix(desktop): preserve authoritative agent avatars (#4984) fix(desktop): next/back navigation during key creation onboarding (#4978) Alert community owners and admins when a new key joins (#4900) fix(desktop): prevent sidebar prefs from reverting on stale-localStorage boot (#5086) chore(hooks): run desktop typecheck in pre-push (#5110) feat(identity): recover desktop identity from a signed-in phone (#4845) fix(buzz-agent): classify read timeouts distinctly in LLM error messages (#4959) Refine agent runtime controls (#5026) test(desktop): await thread scroll anchor (#3174) Improve desktop mobile pairing flow (#5024) feat(desktop): show selected community in rail (#5000) fix(desktop): stop rate-limited reconnect backfill from tearing down the authenticated socket (#4990) fix(desktop): skip native notifications outside app bundles (#5004) ci: prove the relay-driven mesh lifecycle — discover, join, infer, deny — with real nodes (#3862) fix(desktop): virtualize channel member lists (#4991) Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
…igration * origin/main: fix(bench): mention the orchestrator by pubkey when posting the task (#5136) feat(relay): accept kind:30179 private managed-agent events at ingest (#5133) fix(media): require authenticated reads (#4610) fix(desktop): preserve authoritative agent avatars (#4984) fix(desktop): next/back navigation during key creation onboarding (#4978) Alert community owners and admins when a new key joins (#4900) fix(desktop): prevent sidebar prefs from reverting on stale-localStorage boot (#5086) chore(hooks): run desktop typecheck in pre-push (#5110) feat(identity): recover desktop identity from a signed-in phone (#4845) fix(buzz-agent): classify read timeouts distinctly in LLM error messages (#4959) Refine agent runtime controls (#5026) test(desktop): await thread scroll anchor (#3174) Improve desktop mobile pairing flow (#5024) feat(desktop): show selected community in rail (#5000) fix(desktop): stop rate-limited reconnect backfill from tearing down the authenticated socket (#4990) fix(desktop): skip native notifications outside app bundles (#5004) ci: prove the relay-driven mesh lifecycle — discover, join, infer, deny — with real nodes (#3862) Signed-off-by: Brad Seiler <seiler@squareup.com> # Conflicts: # deploy/charts/buzz/templates/deployment.yaml # deploy/charts/buzz/tests/render_test.yaml # deploy/charts/buzz/values.schema.json # deploy/charts/buzz/values.yaml
|
Opened the channel-ACL follow-up this commit called for: #5185. It binds a blob to its originating channel via the sidecar and enforces private-channel membership on reads. Scope caveat, stated up front in the PR description: no client emits the |
Conflict resolutions, upstream-as-default: - config.rs: drop the fork's require_media_get_auth staging flag — upstream block#4610 made authenticated media reads unconditional; keep file_index_enabled. - ingest.rs: kind-list unions (upstream KIND_PRIVATE_MANAGED_AGENT + fork KIND_SWARM/KIND_COMMUNITY_GUIDE). - Link previews (block#3818) x swarm tags: splitOutgoingTags routes both tag kinds; sendChannelMessage carries linkPreviewTags (upstream position) and swarmTags (appended); HomeView caller realigned — positional same-typed params, invisible to tsc. - ChannelPane: upstream WelcomeComposerGuidanceLayer render kept, driven by the fork's useWelcomeComposerBanner state; old Banner import dropped. - swarmDialogState: adopt block#5202 pubkey-keyed agent identity — distinct same-name agents stay separate swarm picker options (collapse made one unpickable); fork test updated. SettingsPanels.tsx grew 930->1010 from upstream's side — over-cap, split owed (same bucket as MembersSidebar). Verified: tsc, desktop suite (4663), cargo check buzz-relay, fmt. Signed-off-by: kbst9 <kevinbsteiner@gmail.com>
## Buzz Relay release v0.2.1 ### Changes since relay-v0.2.0: - fix(sdk): preserve self-mention p tags in message and forum event builders ([#4975](#4975)) ([`78c87ae20e`](78c87ae)) - feat(desktop): adding rich link previews to messages ([#3818](#3818)) ([`1922d49cb2`](1922d49)) - feat(relay): accept kind:30179 private managed-agent events at ingest ([#5133](#5133)) ([`ad923353a2`](ad92335)) - fix(media): require authenticated reads ([#4610](#4610)) ([`769ac70b74`](769ac70)) - feat(identity): recover desktop identity from a signed-in phone ([#4845](#4845)) ([`6eb65919f1`](6eb6591)) - ci: prove the relay-driven mesh lifecycle — discover, join, infer, deny — with real nodes ([#3862](#3862)) ([`38bf642fcf`](38bf642)) - relay: fuzz WebSocket 1012 restart-close timing on graceful drain (BUZZ_DRAIN_JITTER_MS) ([#4542](#4542)) ([`e14fff74d0`](e14fff7)) - fix(reactions): support max-length custom emoji ([#3833](#3833)) ([`2ea9385015`](2ea9385)) - fix(channels): restrict private-channel invitations ([#4612](#4612)) ([`efe1893dd3`](efe1893)) - fix(workflow): bind trigger author to the signed event ([#4607](#4607)) ([`885bed35ee`](885bed3)) - fix(git): revoke access for banned relay members ([#4608](#4608)) ([`997b8caaa4`](997b8ca)) - Define private managed agent wire protocol ([#4593](#4593)) ([`067c085f37`](067c085)) - perf(relay): index channel-id lookups and skip trace-only reads ([#4647](#4647)) ([`bc9e6528a7`](bc9e652)) - Polish mobile inbox and media flows ([#4512](#4512)) ([`feccf4eabc`](feccf4e)) - fix(git): allow deleting the default branch ([#4297](#4297)) ([`fc598f5f8d`](fc598f5)) - feat(projects): add buzz projects CLI commands (NIP-MP kind:30621) ([#4020](#4020)) ([`b7bb15122e`](b7bb151)) - perf(relay): serve relay-membership checks from the read replica ([#4124](#4124)) ([`ac4fa13b8e`](ac4fa13)) - fix(relay): allow open relays to set their NIP-11 workspace icon (kind:9033) ([#3998](#3998)) ([`5765fc74b7`](5765fc7)) - feat(relay): accept kind:30621 multi-repo projects at ingest ([#3171](#3171)) ([`cb9701cd30`](cb9701c)) - feat(relay): raise hosted community limit to five ([#3829](#3829)) ([`10d5a26414`](10d5a26)) - fix(relay): align NIP-11 max_limit with REQ ceiling ([#3635](#3635)) ([`23f0c26b1c`](23f0c26)) - feat(relay): gate kind 30178 team-catalog reads behind the shared tag ([#3358](#3358)) ([`114d40d9d3`](114d40d)) - fix(db): isolate usage metrics advisory-lock test on scratch DB ([#3670](#3670)) ([`dba97eecd9`](dba97ee)) - perf(presence): reduce heartbeat frequency ([#3783](#3783)) ([`bf139e8d0b`](bf139e8)) - feat(mesh): upgrade embedded mesh to v0.74 and harden shared compute (split 1/2 of #3467) ([#3741](#3741)) ([`4933672eb4`](4933672)) - feat(replica): portable heartbeat-token fence with snapshot-local reader routing ([#3268](#3268)) ([`63496cc1d4`](63496cc)) - fix(git): channel binding tooling + author remediation for unbound repos ([#3626](#3626)) ([`788b3c002b`](788b3c0)) - feat: configure S3 URL addressing style ([#3400](#3400)) ([`7012d86d52`](7012d86)) - feat(tracing): correlate trace IDs in relay logs ([#3608](#3608)) ([`005b5b819a`](005b5b8)) - fix(relay): avoid subscription lock inversion ([#3413](#3413)) ([`22be8bb351`](22be8bb)) - feat(cli): add users set-status command for NIP-38 profile status ([#3253](#3253)) ([`60158fce3e`](60158fc)) - feat(relay): make Postgres pool size configurable, default 50 ([#3191](#3191)) ([`2ce2d71cc3`](2ce2d71)) - feat(tracing): add datastore tracing plumbing ([#2760](#2760)) ([`e94b9aeda0`](e94b9ae)) - feat(invites): add use-limited invite links ([#3141](#3141)) ([`d500c2d5cf`](d500c2d)) - feat(admin): show reported message content in report detail ([#3149](#3149)) ([`f069a85503`](f069a85)) - resolve findings ([#3150](#3150)) ([`9b0f744804`](9b0f744)) - Revert "fix(cli,relay): resolve agents by verified owner" ([#3168](#3168)) ([`a041e2d21e`](a041e2d)) - fix(cli,relay): resolve agents by verified owner ([#2615](#2615)) ([`c3084b36d9`](c3084b3)) - fix(security): enforce durable community ban on NIP-43 relay-admin kinds 9030-9033 ([#3128](#3128)) ([`e2e0079101`](e2e0079)) - fix(security): authorize kind:9000 role changes in both directions ([#3017](#3017)) ([`00ecf2cac7`](00ecf2c)) - feat(desktop): handle project work from Inbox ([#3117](#3117)) ([`c5c4f390b6`](c5c4f39)) - feat(relay): make per-owner community limit configurable via BUZZ_MAX_COMMUNITIES_PER_OWNER ([#2599](#2599)) ([`2a051a404d`](2a051a4)) - feat(relay): add author-only-unless-shared read gate for kind 30175 ([#2768](#2768)) ([`ab3af82871`](ab3af82)) - fix(core): block IPv6 transition SSRF targets ([#2801](#2801)) ([`c26bf5945d`](c26bf59)) - fix(workflow): bypass system proxies for webhooks ([#2800](#2800)) ([`60a171b19e`](60a171b)) - fix(audit): hash created_at at the precision Postgres stores ([#2638](#2638)) ([`264a56a226`](264a56a)) - feat(desktop): make pull request reviews actionable ([#2510](#2510)) ([`9081ab0ec9`](9081ab0)) - fix(relay): decompress gzip-encoded git smart-HTTP request bodies ([#2670](#2670)) ([`5ca36e7b91`](5ca36e7)) - fix(sharing): preserve agent/team snapshot tEXt chunks through media sanitization ([#2438](#2438)) ([`b096b0a15a`](b096b0a)) - fix(relay): send 1012 restart close to all clients on graceful drain ([#2575](#2575)) ([`1911c69aa2`](1911c69)) - fix(media): sanitize animated image uploads ([#2524](#2524)) ([`8f8f5fa5a4`](8f8f5fa)) - fix(channels): strip leading hash prefixes from names ([#2250](#2250)) ([`d0ab3fdb05`](d0ab3fd)) - feat(relay): make Redis pool size configurable, default 16 ([#2521](#2521)) ([`bcc3e13069`](bcc3e13)) - feat(desktop+acp): spawn a harness per (agent, community) pair at GUI startup — warm sockets, lazy LLM pool ([#2122](#2122)) ([`61cc738ee8`](61cc738)) - feat(media): add S3-truth per-community storage sweep ([#2044](#2044)) ([`bd37a4d584`](bd37a4d)) - feat(relay): log NIP-98 pubkey attribution on HTTP bridge requests ([#2206](#2206)) ([`7e34bee62c`](7e34bee)) - Revert "feat(relay): inventory unreachable Git objects" ([#2275](#2275)) ([`0fb820f9bf`](0fb820f)) - feat(relay): inventory unreachable Git objects ([#2264](#2264)) ([`3afc9dae15`](3afc9da)) - relay: add author_type label to buzz_events_stored_total ([#2243](#2243)) ([`b9f54c43fe`](b9f54c4)) - fix(git): make project branch workflows reliable ([#2213](#2213)) ([`166f27be4b`](166f27b)) - feat(cli): manage repository protection rules ([#2193](#2193)) ([`f94324598d`](f943245)) - feat(cli): add agents archive/unarchive/archived subcommands ([#2173](#2173)) ([`7d7992067b`](7d79920)) - fix(mobile): sanitize Android image uploads ([#2188](#2188)) ([`ee21da90bd`](ee21da9)) - fix(cli): paginate channel directory queries ([#2181](#2181)) ([`03fe19d603`](03fe19d)) - fix(mobile): image upload fails due to unstripped metadata ([#2185](#2185)) ([`37f15b2001`](37f15b2)) - perf(relay): compact Git packs before manifest limits ([#2172](#2172)) ([`80e0ab16b0`](80e0ab1)) - perf(relay): cache Git pack hydration ([#2169](#2169)) ([`a4d82ec722`](a4d82ec)) - fix(relay): bound and observe Git read operations ([#2167](#2167)) ([`5f7c93d9c1`](5f7c93d)) - relay: gate push enqueue on live leases; batch matcher pipeline (T1b/T1a-repair/T2b) ([#2145](#2145)) ([`e43b2d5aac`](e43b2d5)) - relay: add audit logging disable switch ([#2134](#2134)) ([`bf5acabdde`](bf5acab)) - relay: skip TTL deadline bump for known-permanent channels (T1a write-amp) ([#2125](#2125)) ([`2e936d439c`](2e936d4)) - fix(git): carry NIP-OA delegation in auth event ([#2120](#2120)) ([`c12257d57a`](c12257d)) - Route lag-tolerant reads to an optional Postgres read replica ([#2084](#2084)) ([`29c48883d3`](29c4888)) - fix: recover community access visibility ([#2074](#2074)) ([`ca384d082d`](ca384d0)) - feat: proxy feedback-scoped admin attachments ([#2059](#2059)) ([`d7f918e3cb`](d7f918e)) - feat: add read-only deployment moderation dashboard ([#1999](#1999)) ([`68e670e001`](68e670e)) - Bug-bash round 2: table scroll, Goose instructions, workflow mention wake ([#2034](#2034)) ([`64b8fea6dc`](64b8fea)) - Strip media metadata on clients and reject it at the relay ([#2006](#2006)) ([`5cfd69cb0c`](5cfd69c)) - [codex] Hold Git concurrency permits through streaming (BUZZ-SEC-018) ([#1916](#1916)) ([`7baea42abb`](7baea42)) - [codex] Enforce shared relay admission limits (BUZZ-SEC-019) ([#1917](#1917)) ([`73fc0ec6cf`](73fc0ec)) - [codex] Block banned actors from moderation commands (BUZZ-SEC-007) ([#1915](#1915)) ([`caa195ca58`](caa195c)) - [codex] Fix relay WebSocket admission limits ([#1682](#1682)) ([`d3ce971fc7`](d3ce971)) - feat: add invite QR and mobile direct join ([#1957](#1957)) ([`648cbf3610`](648cbf3)) - fix(join-policy): require legal consent on hosted invites ([#1987](#1987)) ([`2e1577f76f`](2e1577f)) - [codex] Prevent actor-tag UI impersonation ([#1931](#1931)) ([`c540ec9678`](c540ec9)) - Scope relay runtime state by community ([#1658](#1658)) ([`d52dedb06f`](d52dedb)) - Apply optional relay join policy across join flows ([#1894](#1894)) ([`6c2d667575`](6c2d667)) - feat(media): require auth for relay media reads ([#1926](#1926)) ([`f308762852`](f308762)) - feat(relay): add community unarchive endpoint ([#1908](#1908)) ([`6b9641db2b`](6b9641d)) - feat(relay): gate Git web GUI separately ([#1901](#1901)) ([`34dc7dec75`](34dc7de)) - mesh: upgrade runtime, enforce membership, add shared compute provider ([#1656](#1656)) ([`54638ff4bb`](54638ff)) - Route Git scratch through configured volume ([#1884](#1884)) ([`2318b3096c`](2318b30)) - feat(relay): gate usage metrics behind stable leader ([#1814](#1814)) ([`59e9821503`](59e9821)) - Relay mesh: cross-pod tunnel + huddle transport (buzz-relay-mesh) ([#1670](#1670)) ([`ccb021d713`](ccb021d)) - feat(push): deliver accepted relay events as wakes ([#1866](#1866)) ([`bffbc5f22c`](bffbc5f)) - fix(db): resolve duplicate migration version ([#1863](#1863)) ([`08ad38a07f`](08ad38a)) - Add private product feedback sidecar ([#1857](#1857)) ([`af190c93e1`](af190c9)) - feat(relay): add durable community archival ([#1834](#1834)) ([`2b15a72675`](2b15a72)) - feat(push): add public APNs gateway ([#1770](#1770)) ([`1c006822e4`](1c00682)) - feat(relay): add atomic community ownership transfer ([#1845](#1845)) ([`52e42ccb9f`](52e42cc)) - Bound NIP-RS retention and search indexing ([#1771](#1771)) ([`1b4703021d`](1b47030)) - Add optional standalone pairing relay to Helm chart ([#1799](#1799)) ([`9b47c8548f`](9b47c85)) - fix(relay): publish membership snapshot on provisioning ([#1761](#1761)) ([`0950d392b7`](0950d39)) - feat(relay): per-community usage metrics ([#1723](#1723)) ([`620822899a`](6208228)) - refactor(desktop): remove vestigial MCP toolsets config ([#1776](#1776)) ([`dfec75b3c0`](dfec75b)) **To release:** merge this PR. The tag and build will happen automatically. Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…as verified against Upstream relay-v0.2.1 made media GET/HEAD require a signed Blossom authorization and current relay membership (block/buzz#4610), removing the unauthenticated compatibility path. This node already sent that header, so nothing broke — but nothing here would have caught it if the header had been wrong: the offline suite states it does not cover the download path, and the wire test did not either. - Export uploadBlob/downloadBlob so the wire test drives the real code rather than a fetch that reproduces what it assumes the code does. - Teach the test's httpRequest shim the two shapes the media path uses: a Buffer body (JSON.stringify would corrupt an upload into {"type":"Buffer"} and store it under the wrong sha256) and encoding:'stream' + returnFullResponse. - Add three checks: upload, an authenticated download verified by sha256 round-trip, and an unauthenticated download that must be refused. The fixture is a fixed 75-byte generated PNG, so Blossom content-addressing makes every run reuse one blob — the relay has no media-delete API, so a per-run fixture would leak a file each time. - README: document authenticated media reads, state that this does not soften "uploads are publishing" (members still read; a blob is not bound to its channel upstream), and note that the relay behaviours listed were verified against relay-v0.2.1. The unauthenticated-read check is marked in-file as NOT YET SHOWN TO FAIL. It passes against relay-v0.2.1, which proves it runs, not that it would catch a relay that serves media openly. Validating it needs a run against the previous image (48933af5, commit 631b05c8).
This change requires a valid signed Blossom authorization request and current relay membership for every media GET and HEAD request. It removes the unauthenticated compatibility path and updates desktop reads to send the required authorization.
This blocks anonymous retrieval and access after relay-membership revocation. It does not yet bind a blob to its originating channel, so someone removed from a private channel can still read a known blob while remaining a relay member. That channel-ACL follow-up remains required before closing the full finding.
Testing
git diff --check origin/main...codex/security-media-read-authorigin/mainat5c98932Originating Buzz thread:
buzz://message?channel=3928fe05-df61-4b5d-b9c7-d623b9b10ea1&id=3c6c02312f763fbe0d2bfc33a6c1a362f91d0354f3d18b039cf7a0558c1439d1