diff --git a/components/providers/ArweaveFallbackSwRegistration.tsx b/components/providers/ArweaveFallbackSwRegistration.tsx new file mode 100644 index 0000000000..b6a8acc71c --- /dev/null +++ b/components/providers/ArweaveFallbackSwRegistration.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { useEffect } from "react"; + +const SW_PATH = "/arweave-fallback-sw.js"; + +export default function ArweaveFallbackSwRegistration() { + useEffect(() => { + if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) { + return; + } + navigator.serviceWorker + .register(SW_PATH, { scope: "/" }) + .catch((e) => { + if (process.env.NODE_ENV !== "production") { + console.error(`[ArweaveFallbackSW] Failed to register ${SW_PATH}:`, e); + } + }); + }, []); + return null; +} diff --git a/components/providers/Providers.tsx b/components/providers/Providers.tsx index 43c754f224..eea12660a4 100644 --- a/components/providers/Providers.tsx +++ b/components/providers/Providers.tsx @@ -19,6 +19,7 @@ import { WaveEligibilityProvider } from "@/contexts/wave/WaveEligibilityContext" import { AppWebSocketProvider } from "@/services/websocket/AppWebSocketProvider"; import { LayoutProvider } from "../brain/my-stream/layout/LayoutContext"; import { ViewProvider } from "../navigation/ViewContext"; +import ArweaveFallbackSwRegistration from "./ArweaveFallbackSwRegistration"; import CapacitorSetup from "./CapacitorSetup"; import IpfsImageSetup from "./IpfsImageSetup"; import QueryClientSetup from "./QueryClientSetup"; @@ -34,6 +35,7 @@ export default function Providers({ + diff --git a/config/nextConfig.ts b/config/nextConfig.ts index a4cf75f8a0..624b7c783d 100644 --- a/config/nextConfig.ts +++ b/config/nextConfig.ts @@ -19,6 +19,7 @@ export function sharedConfig( { protocol: "https", hostname: "6529.io" }, { protocol: "https", hostname: "staging.6529.io" }, { protocol: "https", hostname: "arweave.net" }, + { protocol: "https", hostname: "ar-io.net" }, { protocol: "http", hostname: "localhost" }, { protocol: "https", hostname: "media.generator.seize.io" }, { protocol: "https", hostname: "d3lqz0a4bldqgf.cloudfront.net" }, diff --git a/config/securityHeaders.ts b/config/securityHeaders.ts index 7b41a5d57a..45a9bfd8de 100644 --- a/config/securityHeaders.ts +++ b/config/securityHeaders.ts @@ -6,7 +6,7 @@ export function createSecurityHeaders(apiEndpoint: string | undefined = "") { }, { key: "Content-Security-Policy", - value: `default-src 'none'; script-src 'self' 'unsafe-inline' https://dnclu2fna0b2b.cloudfront.net https://www.google-analytics.com https://www.googletagmanager.com/ https://dataplane.rum.us-east-1.amazonaws.com 'unsafe-eval'; connect-src * 'self' blob: ${apiEndpoint} https://registry.walletconnect.com/api/v2/wallets wss://*.bridge.walletconnect.org wss://*.walletconnect.com wss://www.walletlink.org/rpc https://explorer-api.walletconnect.com/v3/wallets https://www.googletagmanager.com https://*.google-analytics.com https://cloudflare-eth.com/ https://arweave.net/* https://rpc.walletconnect.com/v1/ https://sts.us-east-1.amazonaws.com https://sts.us-west-2.amazonaws.com; font-src 'self' data: https://fonts.gstatic.com https://fonts.reown.com https://dnclu2fna0b2b.cloudfront.net https://cdnjs.cloudflare.com; img-src 'self' data: blob: ipfs: https://artblocks.io https://*.artblocks.io *; media-src 'self' blob: https://*.cloudfront.net https://videos.files.wordpress.com https://arweave.net https://*.arweave.net https://cf-ipfs.com/ipfs/* https://*.twimg.com https://artblocks.io https://*.artblocks.io; frame-src 'self' https://ipfs.io https://ipfs.io/ipfs/ https://cf-ipfs.com https://cf-ipfs.com/ipfs/ https://media.generator.seize.io https://media.generator.6529.io https://generator.seize.io https://arweave.net https://*.arweave.net https://nftstorage.link https://*.ipfs.nftstorage.link https://verify.walletconnect.com https://verify.walletconnect.org https://secure.walletconnect.com https://d3lqz0a4bldqgf.cloudfront.net https://www.youtube.com https://www.youtube-nocookie.com https://*.youtube.com https://artblocks.io https://*.artblocks.io https://docs.google.com https://drive.google.com https://*.google.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com/css2 https://dnclu2fna0b2b.cloudfront.net https://cdnjs.cloudflare.com http://cdnjs.cloudflare.com https://cdn.jsdelivr.net; object-src data:;`, + value: `default-src 'none'; script-src 'self' 'unsafe-inline' https://dnclu2fna0b2b.cloudfront.net https://www.google-analytics.com https://www.googletagmanager.com/ https://dataplane.rum.us-east-1.amazonaws.com 'unsafe-eval'; connect-src * 'self' blob: ${apiEndpoint} https://registry.walletconnect.com/api/v2/wallets wss://*.bridge.walletconnect.org wss://*.walletconnect.com wss://www.walletlink.org/rpc https://explorer-api.walletconnect.com/v3/wallets https://www.googletagmanager.com https://*.google-analytics.com https://cloudflare-eth.com/ https://arweave.net/* https://ar-io.net https://ar-io.net/* https://rpc.walletconnect.com/v1/ https://sts.us-east-1.amazonaws.com https://sts.us-west-2.amazonaws.com; font-src 'self' data: https://fonts.gstatic.com https://fonts.reown.com https://dnclu2fna0b2b.cloudfront.net https://cdnjs.cloudflare.com; img-src 'self' data: blob: ipfs: https://artblocks.io https://*.artblocks.io *; media-src 'self' blob: https://*.cloudfront.net https://videos.files.wordpress.com https://arweave.net https://*.arweave.net https://ar-io.net https://*.ar-io.net https://cf-ipfs.com/ipfs/* https://*.twimg.com https://artblocks.io https://*.artblocks.io; frame-src 'self' https://ipfs.io https://ipfs.io/ipfs/ https://cf-ipfs.com https://cf-ipfs.com/ipfs/ https://media.generator.seize.io https://media.generator.6529.io https://generator.seize.io https://arweave.net https://*.arweave.net https://ar-io.net https://*.ar-io.net https://nftstorage.link https://*.ipfs.nftstorage.link https://verify.walletconnect.com https://verify.walletconnect.org https://secure.walletconnect.com https://d3lqz0a4bldqgf.cloudfront.net https://www.youtube.com https://www.youtube-nocookie.com https://*.youtube.com https://artblocks.io https://*.artblocks.io https://docs.google.com https://drive.google.com https://*.google.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com/css2 https://dnclu2fna0b2b.cloudfront.net https://cdnjs.cloudflare.com http://cdnjs.cloudflare.com https://cdn.jsdelivr.net; object-src data:;`, }, { key: "X-Frame-Options", value: "SAMEORIGIN" }, { key: "X-Content-Type-Options", value: "nosniff" }, diff --git a/lib/arweave-fallback.ts b/lib/arweave-fallback.ts new file mode 100644 index 0000000000..2b355a627b --- /dev/null +++ b/lib/arweave-fallback.ts @@ -0,0 +1,23 @@ +export const ARWEAVE_HOST = "arweave.net"; +export const FALLBACK_HOST = "ar-io.net"; + +export function isArweaveUrl(url: string): boolean { + try { + const u = new URL(url); + const h = u.hostname.toLowerCase(); + return h === ARWEAVE_HOST || h.endsWith("." + ARWEAVE_HOST); + } catch { + return false; + } +} + +export function getArweaveFallbackUrl(url: string): string | null { + if (!isArweaveUrl(url)) return null; + try { + const u = new URL(url); + u.host = FALLBACK_HOST + (u.port ? ":" + u.port : ""); + return u.toString(); + } catch { + return null; + } +} diff --git a/public/arweave-fallback-sw.js b/public/arweave-fallback-sw.js new file mode 100644 index 0000000000..27b7d8fd04 --- /dev/null +++ b/public/arweave-fallback-sw.js @@ -0,0 +1,37 @@ +self.addEventListener("fetch", (event) => { + const url = new URL(event.request.url); + + if (url.hostname !== "arweave.net") return; + if (event.request.mode === "navigate") return; + + // Safety: only handle normal asset GETs + if (event.request.method !== "GET") return; + if (event.request.headers.has("range")) return; + + event.respondWith(handleRequest(event.request)); +}); + +async function handleRequest(request) { + try { + const response = await fetch(request); + + if (response && (response.type === "opaque" || response.ok)) { + return response; + } + + return fetchFallback(request); + } catch (err) { + return fetchFallback(request); + } +} + +function fetchFallback(request) { + const originalUrl = new URL(request.url); + const fallbackUrl = + "https://ar-io.net" + + originalUrl.pathname + + originalUrl.search + + originalUrl.hash; + + return fetch(fallbackUrl); +}