diff --git a/core/Cargo.lock b/core/Cargo.lock index b97fcdfb..fe7f5725 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "archipelago" -version = "1.7.12-alpha" +version = "1.7.13-alpha" dependencies = [ "anyhow", "archipelago-container", diff --git a/core/archipelago/Cargo.toml b/core/archipelago/Cargo.toml index d75b7af6..74f2bb59 100644 --- a/core/archipelago/Cargo.toml +++ b/core/archipelago/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "archipelago" -version = "1.7.12-alpha" +version = "1.7.13-alpha" edition = "2021" description = "Archipelago Bitcoin Node OS - Native backend" authors = ["Archipelago Team"] diff --git a/core/archipelago/src/api/handler/mod.rs b/core/archipelago/src/api/handler/mod.rs index 3c1f49ed..ce049ab7 100644 --- a/core/archipelago/src/api/handler/mod.rs +++ b/core/archipelago/src/api/handler/mod.rs @@ -113,6 +113,53 @@ impl ApiHandler { } } + /// Server-side fetch of the upstream app catalog so the browser can + /// load it without fighting CORS (git.tx1138.com emits no ACAO) or + /// CSP (the fallback IP-port URL isn't in `connect-src`). Tries the + /// upstream URLs in the same order the frontend used, returns the + /// first 2xx response. 15s total timeout. + async fn handle_app_catalog_proxy() -> Result> { + const UPSTREAMS: &[&str] = &[ + "https://git.tx1138.com/lfg2025/app-catalog/raw/branch/main/catalog.json", + "http://23.182.128.160:3000/lfg2025/app-catalog/raw/branch/main/catalog.json", + ]; + let client = match reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(15)) + .build() + { + Ok(c) => c, + Err(e) => { + return Ok(build_response( + hyper::StatusCode::INTERNAL_SERVER_ERROR, + "text/plain", + hyper::Body::from(format!("client build failed: {}", e)), + )); + } + }; + for url in UPSTREAMS { + match client.get(*url).send().await { + Ok(resp) if resp.status().is_success() => { + if let Ok(bytes) = resp.bytes().await { + return Ok(Response::builder() + .status(hyper::StatusCode::OK) + .header("Content-Type", "application/json") + .header("Cache-Control", "public, max-age=3600") + .body(hyper::Body::from(bytes)) + .unwrap_or_else(|_| { + Response::new(hyper::Body::from("proxy response build failed")) + })); + } + } + _ => continue, + } + } + Ok(build_response( + hyper::StatusCode::BAD_GATEWAY, + "text/plain", + hyper::Body::from("all upstream catalog URLs failed"), + )) + } + /// Build a 401 Unauthorized JSON response. fn unauthorized() -> Response { let body = serde_json::json!({ "error": "Unauthorized" }); @@ -352,6 +399,18 @@ impl ApiHandler { // Electrs status — unauthenticated (read-only sync status) (Method::GET, "/electrs-status") => Self::handle_electrs_status().await, + // App-catalog proxy — fetches catalog.json from the configured + // upstream URLs server-side so the browser doesn't hit CORS + // (git.tx1138.com has no ACAO header) or CSP (IP-port upstream + // falls outside `connect-src`). Session-authenticated so only + // the logged-in node owner can spin up fetches. + (Method::GET, "/api/app-catalog") => { + if !self.is_authenticated(&headers).await { + return Ok(Self::unauthorized()); + } + Self::handle_app_catalog_proxy().await + } + // LND connect info — nginx validates session cookie (presence check), // backend is bound to 127.0.0.1 so only nginx can reach it. // No backend auth check here because the LND UI iframe fetches this diff --git a/neode-ui/src/views/discover/curatedApps.ts b/neode-ui/src/views/discover/curatedApps.ts index b15996f8..36809aa2 100644 --- a/neode-ui/src/views/discover/curatedApps.ts +++ b/neode-ui/src/views/discover/curatedApps.ts @@ -22,13 +22,13 @@ let cachedCatalog: AppCatalog | null = null let catalogFetchedAt = 0 const CATALOG_TTL = 60 * 60 * 1000 // 1 hour cache -/** Remote catalog URLs — tried in order. First success wins. */ +/** Catalog URLs tried in order. First success wins. + * Primary is the backend proxy (`/api/app-catalog`) — server-side fetch + * bypasses CORS on git.tx1138.com and CSP restrictions on the IP-port + * fallback. If the backend is offline (mid-restart etc.) we fall back + * to the static copy baked into the frontend build. */ const CATALOG_URLS = [ - // Primary: git.tx1138.com raw file (HTTPS, dynamic, updated without frontend rebuild) - 'https://git.tx1138.com/lfg2025/app-catalog/raw/branch/main/catalog.json', - // Fallback: direct IP (HTTP, only works if CSP allows http://$host:*) - 'http://23.182.128.160:3000/lfg2025/app-catalog/raw/branch/main/catalog.json', - // Last resort: local static file (baked into frontend build) + '/api/app-catalog', '/catalog.json', ] @@ -40,7 +40,7 @@ export async function fetchAppCatalog(): Promise { for (const url of CATALOG_URLS) { try { - const res = await fetch(url, { signal: AbortSignal.timeout(5000) }) + const res = await fetch(url, { credentials: 'include', signal: AbortSignal.timeout(20000) }) if (!res.ok) continue const data = await res.json() as AppCatalog if (!data.apps?.length) continue diff --git a/releases/manifest.json b/releases/manifest.json index c38a802a..71c5fc58 100644 --- a/releases/manifest.json +++ b/releases/manifest.json @@ -1,25 +1,25 @@ { - "version": "1.7.12-alpha", + "version": "1.7.13-alpha", "release_date": "2026-04-20", "changelog": [ - "Nothing new — version bump so freshly-installed nodes (from the 1.7.11 ISO) have something to OTA down, confirming the end-to-end update pipeline out of the box." + "App catalog now loads reliably. Before, the Marketplace / Discover page couldn't fetch the catalog of apps because the upstream host wasn't sending the right CORS headers and the node's security policy didn't allow the fallback URL either. The node now fetches the catalog server-side and serves it same-origin to the browser — no more blank app lists." ], "components": [ { "name": "archipelago", - "current_version": "1.7.11-alpha", - "new_version": "1.7.12-alpha", - "download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.12-alpha/archipelago", - "sha256": "247f65c2e649332ed67e82faff0d71727f0e272863c2daf4504a5cd954f40df9", - "size_bytes": 40385472 + "current_version": "1.7.12-alpha", + "new_version": "1.7.13-alpha", + "download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.13-alpha/archipelago", + "sha256": "0aaf72625a6cb164b35e30e0dc6f6084cbc96fd8d9da9480b78e85f4b979f22c", + "size_bytes": 40371192 }, { - "name": "archipelago-frontend-1.7.12-alpha.tar.gz", - "current_version": "1.7.11-alpha", - "new_version": "1.7.12-alpha", - "download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.12-alpha/archipelago-frontend-1.7.12-alpha.tar.gz", - "sha256": "0644a43611309031efbb9b235a3602f0828f709fcaec0047543d96e1cbd54f58", - "size_bytes": 76983846 + "name": "archipelago-frontend-1.7.13-alpha.tar.gz", + "current_version": "1.7.12-alpha", + "new_version": "1.7.13-alpha", + "download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.13-alpha/archipelago-frontend-1.7.13-alpha.tar.gz", + "sha256": "27505811ffcae22a33cc895e2dc630b3efef7d0682841eeeea517d5efc6f4142", + "size_bytes": 76982505 } ] } diff --git a/releases/v1.7.13-alpha/archipelago b/releases/v1.7.13-alpha/archipelago new file mode 100755 index 00000000..44ad3ac1 Binary files /dev/null and b/releases/v1.7.13-alpha/archipelago differ diff --git a/releases/v1.7.13-alpha/archipelago-frontend-1.7.13-alpha.tar.gz b/releases/v1.7.13-alpha/archipelago-frontend-1.7.13-alpha.tar.gz new file mode 100644 index 00000000..b53f69c4 Binary files /dev/null and b/releases/v1.7.13-alpha/archipelago-frontend-1.7.13-alpha.tar.gz differ