chore(offline-sync): tighten SW denylist, drop dead /api cache + http localhost CORS

- Service worker (vite-plugin-pwa/Workbox): add /share/, /mcp, and /robots.txt
  to navigateFallbackDenylist so the SPA app-shell never shadows those
  server-rendered routes (they mirror the server static-serve exclude list — the
  share SEO/OG HTML, the MCP endpoint, and robots.txt must come from the server).
- Remove the dead /api GET NetworkFirst Workbox rule (api-get-cache): offline
  reads are served by the persisted TanStack Query cache (IndexedDB) + y-indexeddb,
  never by an SW HTTP cache, so caching GET /api only risked stale responses. All
  /api is now NetworkOnly. clearOfflineCache still deletes any legacy api-get-cache
  defensively (comment updated to note it is no longer created).
- CORS: drop the cleartext 'http://localhost' native-WebView origin. The Capacitor
  shell uses the secure scheme (capacitor.config cleartext:false, default Android
  scheme https, iOS hosted via CAP_SERVER_URL), so no native client uses it;
  allowing it only widened the credentialed-CORS surface. Keeps capacitor://,
  ionic://, and https://localhost.
- docs/mobile-bootstrap.md: replace the inaccurate 'hand-rolled service worker'
  description with the real Workbox generateSW setup (prompt registration via
  virtual:pwa-register, production-only, denylist, NetworkOnly, RQ/y-indexeddb
  offline reads) and drop http://localhost from the CORS origins list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-22 02:44:44 +03:00
parent 9b636bac5a
commit fa4753643c
5 changed files with 65 additions and 30 deletions

View File

@@ -3,7 +3,6 @@ import { buildCorsAllowlist, isOriginAllowed } from './cors.util';
const WEBVIEW_ORIGINS = [
'capacitor://localhost',
'ionic://localhost',
'http://localhost',
'https://localhost',
];
@@ -34,6 +33,13 @@ describe('isOriginAllowed', () => {
expect(isOriginAllowed('https://evil.example', allowlist)).toBe(false);
});
it('rejects the cleartext http://localhost origin', () => {
// The native shell uses the secure scheme (https://localhost) on Android and
// the capacitor:// custom scheme on iOS, so cleartext http://localhost must
// not be trusted.
expect(isOriginAllowed('http://localhost', allowlist)).toBe(false);
});
it('rejects a trailing-slash mismatch', () => {
expect(isOriginAllowed('https://app.example/', allowlist)).toBe(false);
});
@@ -70,11 +76,11 @@ describe('buildCorsAllowlist', () => {
configuredOrigins: ['https://app.example'],
});
// app URL + 4 WebView origins, the duplicate configured origin collapses.
// app URL + WebView origins, the duplicate configured origin collapses.
expect(allowlist.size).toBe(1 + WEBVIEW_ORIGINS.length);
});
it('always includes the four WebView origins even with no configured origins', () => {
it('always includes every WebView origin even with no configured origins', () => {
const allowlist = buildCorsAllowlist({
appUrl: 'https://app.example',
configuredOrigins: [],

View File

@@ -5,12 +5,23 @@
// allowlist (apart from no-Origin requests) is rejected.
// Native WebView origins used by the Capacitor/Ionic mobile shell. Always
// trusted so the native client can call the API. CORS hardening of these is
// intentionally out of scope.
// trusted so the native client can call the API.
//
// - `capacitor://localhost` — iOS native custom scheme.
// - `ionic://localhost` — legacy native custom scheme.
// - `https://localhost` — Android default secure scheme.
//
// The cleartext `http://localhost` origin is intentionally NOT trusted: the
// Capacitor shell uses the secure scheme (capacitor.config.ts sets
// `cleartext: false` and does not override `androidScheme`, so Capacitor's
// default Android scheme is `https` => origin `https://localhost`), and iOS runs
// in hosted mode (`server.url` = CAP_SERVER_URL, whose origin is the app URL
// already in the allowlist). No native client legitimately uses
// `http://localhost`, so allowing it would only widen the credentialed-CORS
// surface to arbitrary local http content.
const NATIVE_WEBVIEW_ORIGINS = [
'capacitor://localhost',
'ionic://localhost',
'http://localhost',
'https://localhost',
] as const;