gateway: prevent stale web UI assets on localhost
This commit is contained in:
@@ -295,7 +295,7 @@ describe('GatewayServer integration', () => {
|
|||||||
const res = await fetch(`http://127.0.0.1:${TEST_PORT}/`);
|
const res = await fetch(`http://127.0.0.1:${TEST_PORT}/`);
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.headers.get('content-type')).toBe('text/html');
|
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();
|
const body = await res.text();
|
||||||
expect(body).toContain('Flynn');
|
expect(body).toContain('Flynn');
|
||||||
});
|
});
|
||||||
@@ -307,6 +307,7 @@ describe('GatewayServer integration', () => {
|
|||||||
const res = await fetch(`http://127.0.0.1:${TEST_PORT}/style.css`);
|
const res = await fetch(`http://127.0.0.1:${TEST_PORT}/style.css`);
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.headers.get('content-type')).toBe('text/css');
|
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 () => {
|
it('serves sw.js with no-store cache policy', async () => {
|
||||||
|
|||||||
+22
-4
@@ -16,6 +16,25 @@ const CONTENT_TYPES: Record<string, string> = {
|
|||||||
'.woff2': 'font/woff2',
|
'.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.
|
* Serve static files from a directory.
|
||||||
*
|
*
|
||||||
@@ -72,10 +91,9 @@ export async function serveStatic(
|
|||||||
try {
|
try {
|
||||||
const content = await readFile(filePath);
|
const content = await readFile(filePath);
|
||||||
const headers: Record<string, string> = { 'Content-Type': contentType };
|
const headers: Record<string, string> = { 'Content-Type': contentType };
|
||||||
if (pathname === '/sw.js') {
|
const cacheControl = resolveCacheControl(pathname, ext);
|
||||||
headers['Cache-Control'] = 'no-store';
|
if (cacheControl) {
|
||||||
} else if (pathname === '/index.html') {
|
headers['Cache-Control'] = cacheControl;
|
||||||
headers['Cache-Control'] = 'no-cache';
|
|
||||||
}
|
}
|
||||||
res.writeHead(200, headers);
|
res.writeHead(200, headers);
|
||||||
res.end(content);
|
res.end(content);
|
||||||
|
|||||||
@@ -13,6 +13,27 @@ function withToken(path) {
|
|||||||
return `${path}${separator}token=${encodeURIComponent(token)}`;
|
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) {
|
async function requestJson(path, options) {
|
||||||
const response = await fetch(withToken(path), {
|
const response = await fetch(withToken(path), {
|
||||||
...(options ?? {}),
|
...(options ?? {}),
|
||||||
@@ -60,6 +81,12 @@ export async function registerPwaServiceWorker() {
|
|||||||
if (!('serviceWorker' in navigator)) {
|
if (!('serviceWorker' in navigator)) {
|
||||||
return null;
|
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'));
|
const registration = await navigator.serviceWorker.register(withToken('/sw.js'));
|
||||||
// Ask the browser to check for a newer worker on app load.
|
// Ask the browser to check for a newer worker on app load.
|
||||||
void registration.update().catch(() => undefined);
|
void registration.update().catch(() => undefined);
|
||||||
|
|||||||
Reference in New Issue
Block a user