From 2a285aa05cdd137070c0a2905d9c38efb23dfac9 Mon Sep 17 00:00:00 2001 From: Wes Date: Thu, 23 Apr 2026 10:54:24 -0700 Subject: [PATCH] fix(desktop): only proxy relay-origin Blossom URLs in media rewriter rewriteRelayUrl() was matching ANY Blossom-patterned URL regardless of domain, then proxying it through the Sprout relay. External Blossom avatar URLs (e.g. from nostr.build) got fetched from the wrong server, resulting in 404s and missing avatars on desktop while mobile loaded them fine. Now fetches and caches the relay origin via get_relay_http_url at module init. Only URLs matching the relay origin are proxied; external Blossom URLs pass through unchanged to WKWebView. Co-Authored-By: Claude Opus 4.6 (1M context) --- desktop/src/shared/lib/mediaUrl.ts | 39 ++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/desktop/src/shared/lib/mediaUrl.ts b/desktop/src/shared/lib/mediaUrl.ts index 6c425c9b..828fc8e6 100644 --- a/desktop/src/shared/lib/mediaUrl.ts +++ b/desktop/src/shared/lib/mediaUrl.ts @@ -8,11 +8,11 @@ * For video, the proxy streams via axum — no buffering the entire file. * Images and other media also benefit from this path. * - * Detection is path-based: /media/{64-hex-chars}.{ext} is a Blossom BUD-01 - * content-addressed URL. The 64-char lowercase hex SHA-256 hash makes this - * pattern unique to Blossom relays — false positives from other origins are - * practically impossible. This avoids needing async relay-URL initialization, - * eliminating race conditions with first render. + * Only URLs hosted on the Sprout relay are rewritten. External Blossom URLs + * (e.g. nostr.build, void.cat) are returned unchanged — they aren't behind + * Cloudflare Access and can be loaded directly by WKWebView. Without this + * origin check, external Blossom URLs would be proxied to the wrong server + * (the Sprout relay), resulting in 404s. */ import { invoke } from "@tauri-apps/api/core"; @@ -26,14 +26,27 @@ const RELAY_MEDIA_RE = let cachedPort: number | null = null; let portPromise: Promise | null = null; +/** Cached relay origin (e.g. "https://sprout-oss.stage.blox.sqprod.co"). */ +let cachedRelayOrigin: string | null = null; + const POLL_INTERVAL_MS = 100; const POLL_TIMEOUT_MS = 5000; /** * Poll `get_media_proxy_port` until we get a non-zero port or timeout. + * Also fetches the relay HTTP base URL for origin-checking. * Returns the port, or null if the proxy never came up. */ async function fetchProxyPort(): Promise { + // Fetch relay origin in parallel — fire-and-forget, no retry needed. + if (!cachedRelayOrigin) { + invoke("get_relay_http_url") + .then((url) => { + cachedRelayOrigin = url.replace(/\/+$/, ""); + }) + .catch(() => {}); + } + const deadline = Date.now() + POLL_TIMEOUT_MS; while (Date.now() < deadline) { try { @@ -58,14 +71,24 @@ if (typeof window !== "undefined") { } /** - * If `url` looks like a Blossom relay media URL, rewrite it to go through - * the localhost streaming proxy. Falls back to sprout-media:// if the proxy - * port isn't available yet. + * If `url` is a Blossom media URL hosted on the Sprout relay, rewrite it + * to go through the localhost streaming proxy. External Blossom URLs and + * non-Blossom URLs are returned unchanged. + * + * Falls back to sprout-media:// if the proxy port isn't available yet. */ export function rewriteRelayUrl(url: string): string { const m = RELAY_MEDIA_RE.exec(url); if (!m) return url; + // Only proxy URLs that belong to our relay. External Blossom URLs + // (different origin) pass through unchanged — they work fine via WKWebView. + // If the relay origin isn't cached yet, fall through to the rewrite path + // as a safe default (relay URLs need the proxy to avoid Cloudflare 403s). + if (cachedRelayOrigin && !url.startsWith(`${cachedRelayOrigin}/`)) { + return url; + } + if (cachedPort && cachedPort > 0) { return `http://localhost:${cachedPort}/media/${m[1]}`; }