Files
gitmost/apps/client/public/sw.js
claude_code caeb555039 feat(mobile): bootstrap mobile app (PWA + Capacitor + backend auth/CORS)
Implements the §12 bootstrap from docs/mobile-app-plan.md.

Backend (§6):
- auth: optional returnToken flag on login returns the JWT in the body
  (data.authToken) for native Keychain/Keystore + Bearer; web cookie flow
  unchanged.
- main.ts: explicit CORS allowlist (APP_URL + CORS_ALLOWED_ORIGINS env +
  Capacitor WebView origins), credentials enabled, replaces open enableCors().
- optional OpenAPI/Swagger at /api/docs behind SWAGGER_ENABLED.
- env: CORS_ALLOWED_ORIGINS, SWAGGER_ENABLED, CAP_SERVER_URL.

PWA:
- manifest metadata, hand-rolled service worker (network-first nav, SWR
  assets, never intercepts /api,/socket.io,/collab), prod-only registration,
  apple-touch-icon.

Capacitor:
- capacitor.config.ts (webDir apps/client/dist; iOS via CAP_SERVER_URL to
  avoid bundling the AGPL client in the .ipa, see plan §9), cap:* scripts,
  deps, .gitignore for native dirs.
- docs/mobile-bootstrap.md documenting what is done and the remaining manual
  steps (cap add ios/android, APNs/FCM, stores).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 15:15:50 +03:00

83 lines
2.5 KiB
JavaScript

// Gitmost PWA service worker.
// Conservative strategy:
// - Never intercept API, websocket or collaboration traffic (always network).
// - Navigations: network-first, fall back to the cached app shell offline.
// - Other same-origin GET assets: stale-while-revalidate.
// Bump CACHE_VERSION to invalidate stale assets on deploy.
const CACHE_VERSION = "gitmost-v1";
const APP_SHELL_URL = "/";
// Path prefixes that must always hit the network (auth/state/realtime).
const NETWORK_ONLY_PREFIXES = ["/api", "/socket.io", "/collab"];
self.addEventListener("install", (event) => {
// Activate this worker immediately without waiting for old tabs to close.
self.skipWaiting();
event.waitUntil(
caches
.open(CACHE_VERSION)
.then((cache) => cache.add(APP_SHELL_URL))
.catch(() => {}),
);
});
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
const keys = await caches.keys();
await Promise.all(
keys
.filter((key) => key !== CACHE_VERSION)
.map((key) => caches.delete(key)),
);
await self.clients.claim();
})(),
);
});
self.addEventListener("fetch", (event) => {
const { request } = event;
// Only handle same-origin GET requests; everything else goes to the network.
if (request.method !== "GET") return;
const url = new URL(request.url);
if (url.origin !== self.location.origin) return;
if (NETWORK_ONLY_PREFIXES.some((prefix) => url.pathname.startsWith(prefix)))
return;
// Navigations: network-first with an offline fallback to the cached shell.
if (request.mode === "navigate") {
event.respondWith(
(async () => {
try {
return await fetch(request);
} catch {
const cache = await caches.open(CACHE_VERSION);
const cached = await cache.match(APP_SHELL_URL);
return cached || Response.error();
}
})(),
);
return;
}
// Static assets: stale-while-revalidate.
event.respondWith(
(async () => {
const cache = await caches.open(CACHE_VERSION);
const cached = await cache.match(request);
const network = fetch(request)
.then((response) => {
// Only cache successful, same-origin (basic) responses.
if (response && response.status === 200 && response.type === "basic") {
cache.put(request, response.clone());
}
return response;
})
.catch(() => undefined);
return cached || (await network) || Response.error();
})(),
);
});