fix: constrain PWA caching and extend font CSP
Quality / quality (ubuntu-latest) (push) Failing after 22s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-03 00:38:23 +05:30
parent b3e9b99e13
commit 649e3d9127
4 changed files with 47 additions and 3 deletions
+2 -1
View File
@@ -53,7 +53,8 @@ export function generateServiceWorker(options: ServiceWorkerOptions = {}): strin
const rules = options.runtimeCaching ?? [
{ pattern: "^https?://", strategy: "network-first" as const, methods: ["GET"] },
];
return `const CACHE=${JSON.stringify(options.cacheName ?? "wrnexus-pwa-v1")};const OFFLINE=${JSON.stringify(offline)};const PRECACHE=${JSON.stringify(urls)};const RULES=${JSON.stringify(rules)};self.addEventListener("install",event=>{event.waitUntil(caches.open(CACHE).then(cache=>cache.addAll(PRECACHE)));self.skipWaiting()});self.addEventListener("activate",event=>event.waitUntil(caches.keys().then(keys=>Promise.all(keys.filter(key=>key.startsWith("wrnexus-pwa-")&&key!==CACHE).map(key=>caches.delete(key)))).then(()=>self.clients.claim())));const networkFirst=async request=>{try{const response=await fetch(request);if(response.ok){const cache=await caches.open(CACHE);await cache.put(request,response.clone())}return response}catch{return(await caches.match(request))||(request.mode==="navigate"?await caches.match(OFFLINE):Response.error())}};const cacheFirst=async request=>(await caches.match(request))||networkFirst(request);const stale=async request=>{const hit=await caches.match(request);const update=networkFirst(request);return hit||(await update)};self.addEventListener("fetch",event=>{const rule=RULES.find(item=>(item.methods||["GET"]).includes(event.request.method)&&new RegExp(item.pattern).test(event.request.url));if(!rule)return;event.respondWith(rule.strategy==="cache-first"?cacheFirst(event.request):rule.strategy==="stale-while-revalidate"?stale(event.request):networkFirst(event.request))});self.addEventListener("sync",event=>{if(event.tag===${JSON.stringify(options.backgroundSyncTag ?? "wrnexus-offline-sync")})event.waitUntil(self.clients.matchAll().then(clients=>clients.forEach(client=>client.postMessage({type:"wrnexus:background-sync"}))))});self.addEventListener("push",event=>{const data=event.data?.json?.()||{};event.waitUntil(self.registration.showNotification(data.title||"Notification",{body:data.body,icon:data.icon,data:{url:data.url||"/"}}))});self.addEventListener("notificationclick",event=>{event.notification.close();event.waitUntil(clients.openWindow(event.notification.data?.url||"/"))});`;
const sameOriginOnly = options.runtimeCaching === undefined;
return `const CACHE=${JSON.stringify(options.cacheName ?? "wrnexus-pwa-v1")};const OFFLINE=${JSON.stringify(offline)};const PRECACHE=${JSON.stringify(urls)};const RULES=${JSON.stringify(rules)};const SAME_ORIGIN_ONLY=${JSON.stringify(sameOriginOnly)};self.addEventListener("install",event=>{event.waitUntil(caches.open(CACHE).then(cache=>cache.addAll(PRECACHE)));self.skipWaiting()});self.addEventListener("activate",event=>event.waitUntil(caches.keys().then(keys=>Promise.all(keys.filter(key=>key.startsWith("wrnexus-pwa-")&&key!==CACHE).map(key=>caches.delete(key)))).then(()=>self.clients.claim())));const networkFirst=async request=>{try{const response=await fetch(request);if(response.ok){const cache=await caches.open(CACHE);await cache.put(request,response.clone())}return response}catch{return(await caches.match(request))||(request.mode==="navigate"?await caches.match(OFFLINE):Response.error())}};const cacheFirst=async request=>(await caches.match(request))||networkFirst(request);const stale=async request=>{const hit=await caches.match(request);const update=networkFirst(request);return hit||(await update)};self.addEventListener("fetch",event=>{if(SAME_ORIGIN_ONLY&&new URL(event.request.url).origin!==self.location.origin)return;const rule=RULES.find(item=>(item.methods||["GET"]).includes(event.request.method)&&new RegExp(item.pattern).test(event.request.url));if(!rule)return;event.respondWith(rule.strategy==="cache-first"?cacheFirst(event.request):rule.strategy==="stale-while-revalidate"?stale(event.request):networkFirst(event.request))});self.addEventListener("sync",event=>{if(event.tag===${JSON.stringify(options.backgroundSyncTag ?? "wrnexus-offline-sync")})event.waitUntil(self.clients.matchAll().then(clients=>clients.forEach(client=>client.postMessage({type:"wrnexus:background-sync"}))))});self.addEventListener("push",event=>{const data=event.data?.json?.()||{};event.waitUntil(self.registration.showNotification(data.title||"Notification",{body:data.body,icon:data.icon,data:{url:data.url||"/"}}))});self.addEventListener("notificationclick",event=>{event.notification.close();event.waitUntil(clients.openWindow(event.notification.data?.url||"/"))});`;
}
export interface OfflineMutation<T = unknown> {
id: string;
+7
View File
@@ -17,6 +17,13 @@ describe("PWA platform", () => {
const source = generateServiceWorker({ offlineUrl: "/offline", cacheUrls: ["/app.css"] });
expect(source).toContain("wrnexus:background-sync");
expect(source).toContain("notificationclick");
expect(source).toContain("const SAME_ORIGIN_ONLY=true");
expect(source).toContain("new URL(event.request.url).origin!==self.location.origin");
expect(
generateServiceWorker({
runtimeCaching: [{ pattern: "^https://cdn.example.com/", strategy: "cache-first" }],
}),
).toContain("const SAME_ORIGIN_ONLY=false");
expect(pwaClientRuntime()).toContain("wrnexus:pwa-installable");
});
test("queues, retries and removes successful mutations", async () => {
+6 -2
View File
@@ -514,7 +514,8 @@ export async function loadAppConfig(appRoot: string, profile?: string): Promise<
/**
* Auto-extend the CSP so configured Google Fonts load under the default policy
* (their CSS host into `style-src`, the static host into `font-src`). No-op when
* (their CSS host into `style-src`, the static host into `font-src`, and the
* static host into `connect-src` for service-worker fetch interception). No-op when
* the app disabled CSP (`security.contentSecurityPolicy: false`) or uses no
* Google Fonts. Local (self-hosted) fonts are served from `'self'` and need nothing.
*/
@@ -532,7 +533,10 @@ function applyFontCsp(config: AppConfig): void {
dirs[name] = Array.from(new Set([...cur, ...adds]));
};
if (add.style.length) extend("style-src", ["'self'", "'unsafe-inline'"], add.style);
if (add.font.length) extend("font-src", ["'self'", "data:"], add.font);
if (add.font.length) {
extend("font-src", ["'self'", "data:"], add.font);
extend("connect-src", ["'self'", "ws:", "wss:"], add.font);
}
}
/**
+32
View File
@@ -196,6 +196,38 @@ test("loadAppConfig loads env before evaluating the config module", async () =>
}
});
test("Google Fonts CSP supports direct fonts and service-worker fetches", async () => {
const dir = mkdtempSync(join(tmpdir(), "wrnexus-font-csp-"));
try {
writeFileSync(
join(dir, "wrnexus.config.ts"),
`export default {
fonts: { google: [{ family: "Inter" }] },
security: {
contentSecurityPolicy: {
directives: { "connect-src": ["'self'", "https://api.example.com"] }
}
}
};\n`,
);
const config = await loadAppConfig(dir, "production");
const directives =
config.security?.contentSecurityPolicy === false
? undefined
: config.security?.contentSecurityPolicy?.directives;
expect(directives?.["style-src"]).toContain("https://fonts.googleapis.com");
expect(directives?.["font-src"]).toContain("https://fonts.gstatic.com");
expect(directives?.["connect-src"]).toEqual([
"'self'",
"https://api.example.com",
"https://fonts.gstatic.com",
]);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("package-aware styles add relative imports and Tailwind scan sources", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-styles-"));
try {