From b94060aec1c4b7f322146a0a62b913afe4f4fc46 Mon Sep 17 00:00:00 2001 From: jubnl Date: Mon, 4 May 2026 21:54:54 +0200 Subject: [PATCH] fix: pass through reverse-proxy auth redirects in service worker Navigation requests now use redirect:'manual' + network-first so upstream auth gates (Cloudflare Zero Trust, Pangolin) can redirect the browser to their SSO login page instead of being swallowed by the precached app shell. An authRedirectPlugin on the API NetworkFirst strategy handles mid-session expiry: detects opaqueredirect responses and converts them to a synthetic 401 { code: 'AUTH_REQUIRED' } that the existing Axios interceptor picks up, triggering a full re-auth flow. Offline fallback to the precached app shell is preserved. Closes #836 --- client/src/sw.ts | 43 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/client/src/sw.ts b/client/src/sw.ts index d57720f4..ae8d742f 100644 --- a/client/src/sw.ts +++ b/client/src/sw.ts @@ -4,7 +4,7 @@ import { clientsClaim } from 'workbox-core'; import { precacheAndRoute, cleanupOutdatedCaches, - createHandlerBoundToURL, + matchPrecache, } from 'workbox-precaching'; import { registerRoute, NavigationRoute } from 'workbox-routing'; import { NetworkFirst, CacheFirst } from 'workbox-strategies'; @@ -23,16 +23,28 @@ self.skipWaiting(); clientsClaim(); // Inject precache manifest (replaced by vite-plugin-pwa at build time) -// @ts-expect-error __WB_MANIFEST is injected at build time precacheAndRoute(self.__WB_MANIFEST); cleanupOutdatedCaches(); // ── Static routes (not user-configurable) ───────────────────────────────────── +// Network-first navigations so reverse-proxy auth redirects (Cloudflare Zero +// Trust, Pangolin, etc.) reach the browser instead of being swallowed by the +// precached app shell. `redirect: 'manual'` produces an opaqueredirect Response +// which, per Fetch spec, the browser follows for navigation requests returned +// from FetchEvent.respondWith. Falls back to precached app shell offline. registerRoute( - new NavigationRoute(createHandlerBoundToURL('index.html'), { - denylist: [/^\/api/, /^\/uploads/, /^\/mcp/], - }), + new NavigationRoute( + async ({ request }) => { + try { + return await fetch(request, { redirect: 'manual' }); + } catch { + const cached = await matchPrecache('index.html'); + return cached ?? Response.error(); + } + }, + { denylist: [/^\/api/, /^\/uploads/, /^\/mcp/] }, + ), ); registerRoute( @@ -65,11 +77,32 @@ registerRoute( const DAY = 24 * 60 * 60; +// Detects when an upstream reverse-proxy auth gate (Cloudflare Zero Trust, +// Pangolin, etc.) redirects a mid-session API call to an external SSO login +// page. Uses redirect:'manual' so the response stays as opaqueredirect instead +// of being silently followed; converts it to a 401 that the Axios interceptor +// in api/client.ts already handles (→ window.location.href = '/login'). +const authRedirectPlugin = { + async requestWillFetch({ request }: { request: Request }): Promise { + return new Request(request, { redirect: 'manual' }); + }, + async fetchDidSucceed({ response }: { response: Response }): Promise { + if (response.type === 'opaqueredirect') { + return new Response(JSON.stringify({ code: 'AUTH_REQUIRED' }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }); + } + return response; + }, +}; + function buildApiStrategy(cfg: SwCacheConfig): NetworkFirst { return new NetworkFirst({ cacheName: 'api-data', networkTimeoutSeconds: 5, plugins: [ + authRedirectPlugin, new ExpirationPlugin({ maxEntries: cfg.apiMaxEntries, maxAgeSeconds: cfg.apiTtlDays * DAY,