gateway: prevent stale web UI assets on localhost

This commit is contained in:
William Valentin
2026-02-18 22:30:47 -08:00
parent 9a0fe3ec56
commit dcbb4649a2
3 changed files with 51 additions and 5 deletions
+2 -1
View File
@@ -295,7 +295,7 @@ describe('GatewayServer integration', () => {
const res = await fetch(`http://127.0.0.1:${TEST_PORT}/`);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe('text/html');
expect(res.headers.get('cache-control')).toBe('no-cache');
expect(res.headers.get('cache-control')).toBe('no-store');
const body = await res.text();
expect(body).toContain('Flynn');
});
@@ -307,6 +307,7 @@ describe('GatewayServer integration', () => {
const res = await fetch(`http://127.0.0.1:${TEST_PORT}/style.css`);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe('text/css');
expect(res.headers.get('cache-control')).toBe('no-store');
});
it('serves sw.js with no-store cache policy', async () => {
+22 -4
View File
@@ -16,6 +16,25 @@ const CONTENT_TYPES: Record<string, string> = {
'.woff2': 'font/woff2',
};
function resolveCacheControl(pathname: string, ext: string): string | null {
if (pathname === '/sw.js') {
// Always fetch the latest worker script so clients can update immediately.
return 'no-store';
}
if (pathname === '/index.html' || pathname === '/chat.html') {
// Prevent stale shell HTML from pinning old asset references.
return 'no-store';
}
if (ext === '.js' || ext === '.mjs' || ext === '.css') {
// Dashboard/frontend code changes frequently in local deployments.
return 'no-store';
}
return null;
}
/**
* Serve static files from a directory.
*
@@ -72,10 +91,9 @@ export async function serveStatic(
try {
const content = await readFile(filePath);
const headers: Record<string, string> = { 'Content-Type': contentType };
if (pathname === '/sw.js') {
headers['Cache-Control'] = 'no-store';
} else if (pathname === '/index.html') {
headers['Cache-Control'] = 'no-cache';
const cacheControl = resolveCacheControl(pathname, ext);
if (cacheControl) {
headers['Cache-Control'] = cacheControl;
}
res.writeHead(200, headers);
res.end(content);
+27
View File
@@ -13,6 +13,27 @@ function withToken(path) {
return `${path}${separator}token=${encodeURIComponent(token)}`;
}
function isLocalhostHost(hostname) {
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
}
async function unregisterAllServiceWorkers() {
if (!('serviceWorker' in navigator)) {
return;
}
const registrations = await navigator.serviceWorker.getRegistrations();
await Promise.all(registrations.map((registration) => registration.unregister()));
}
async function clearFlynnUiCaches() {
if (!('caches' in globalThis)) {
return;
}
const cacheKeys = await caches.keys();
const targets = cacheKeys.filter((key) => key.startsWith('flynn-webchat-'));
await Promise.all(targets.map((key) => caches.delete(key)));
}
async function requestJson(path, options) {
const response = await fetch(withToken(path), {
...(options ?? {}),
@@ -60,6 +81,12 @@ export async function registerPwaServiceWorker() {
if (!('serviceWorker' in navigator)) {
return null;
}
if (isLocalhostHost(window.location.hostname)) {
// Local dev/runtime should always reflect latest dashboard assets.
await unregisterAllServiceWorkers();
await clearFlynnUiCaches();
return null;
}
const registration = await navigator.serviceWorker.register(withToken('/sw.js'));
// Ask the browser to check for a newer worker on app load.
void registration.update().catch(() => undefined);