release(v1.7.13-alpha): proxy app catalog server-side (CORS + CSP fix)

The Discover / Marketplace page fetched the app catalog directly from
git.tx1138.com/lfg2025/app-catalog/raw/.../catalog.json in the
browser. Two blockers hit the fleet simultaneously: (1) tx1138's
Gitea doesn't emit Access-Control-Allow-Origin so the HTTPS fetch
got CORS-blocked; (2) the HTTP IP-port fallback
(http://23.182.128.160:3000/...) falls outside the node's
`connect-src` CSP. Users saw the hardcoded fallback instead of the
live catalog.

Backend: new authenticated GET /api/app-catalog handler uses reqwest
to pull catalog.json server-side (15s timeout) and returns it with
application/json + 1h Cache-Control. Tries the HTTPS URL first,
HTTP IP-port second.

Frontend: curatedApps.ts now calls /api/app-catalog (same-origin,
no CORS/CSP) with credentials included so the session cookie
authenticates the proxy. Baked /catalog.json stays as the last
resort.

Artefacts:
  archipelago                                      0aaf7262…b979f22c  40371192
  archipelago-frontend-1.7.13-alpha.tar.gz         27505811…efc6f4142 76982505

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-20 15:43:45 -04:00
parent 26630e5ffd
commit 687c216e65
4 changed files with 68 additions and 9 deletions

View File

@@ -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<AppCatalog | null> {
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