[grade=A urn:ump:qszwspay7vhx7wcpqekkkidni3ehhgidvze5qk2y22fvg2wqz3ja] fix(DC-170): reliability batch — hangs, atomicity, fail-closed, SW (audit batch 6)

8 reliability fixes across 8 files (AUDIT-2026-10-27.md findings
9/11/12/13/14/16/20 + calendar credential item):

- dns.js check-update/update HANGS: bare returns + empty else/catch
  branches left requests hanging until client timeout; every path now
  answers (502 with message); upstream JSON.parse wrapped (non-JSON ->
  502)
- calendar.js ICS fetch timeout actually applies: fetchT ignores
  opts.timeout (undici) — was running at 5s default, not the intended
  15s; now timeoutMs third arg
- calendar.js PUT /sources: updating pass no longer nulls the stored
  token (and vice versa) — existing credential record loaded, only the
  provided field overwritten
- state-manager.js ATOMIC WRITES: _atomicWrite = tmp file + fs.rename
  (POSIX-atomic); write()/update() no longer truncate-then-write the
  live file (crash mid-write corrupted services.json/config.json with
  no recovery path). Test mock extended to the tmp+rename contract
- dashboard-ws.js auth fallback FAIL-CLOSED: missing authVerifier used
  to accept any non-empty session cookie (DC-061 footgun on unwired
  boot paths); now rejects + loud CRITICAL log
- middleware.js tailscale gate: new tailscale.failClosed config (default
  off = unchanged); when set, outages/errors deny (503) instead of
  silently disabling the gate; fail-open path now logs the exact key
- sw.js: offline fallback actually works ((await match) || (await
  match) — old Promise||Promise was always truthy); only 2xx cached
  (5xx pages were stored and served as the app shell); install is
  per-URL tolerant (one bad asset kept the stale SW alive forever)
- config/settings.js GET /config (public): corrupt config.json degrades
  to {setupComplete:false, corrupt:true} instead of 500 on every load

SCOPE CORRECTION: audit finding #10 (7 reversed-arg errorResponse
sites) was a FALSE POSITIVE — those files alias message-first error();
see lesson #14. Full suite 173/173 suites, 3740/3740 tests.
This commit is contained in:
Hermes committed 2026-10-29 01:46:13 -07:00
1 parent f4f2c26de3
commit 6b7ca4b3e9
8 files changed
+115 -31

No files matched your search

+14 -5
View File
@@ -53,14 +53,18 @@ async function networkFirst(request, preloadResponsePromise) {
try {
const preloadResponse = preloadResponsePromise ? await preloadResponsePromise : null;
if (preloadResponse) {
cache.put(request, preloadResponse.clone()).catch(() => {});
if (preloadResponse.ok) cache.put(request, preloadResponse.clone()).catch(() => {});
return preloadResponse;
}
const response = await fetch(request);
cache.put(request, response.clone()).catch(() => {});
// DC-170: only cache GOOD responses — a 5xx was being stored and later
// served as the app shell by both strategies.
if (response.ok) cache.put(request, response.clone()).catch(() => {});
return response;
} catch (_) {
return caches.match(request) || caches.match('/index.html');
// DC-170: `caches.match(x) || caches.match(y)` returned a Promise (always
// truthy) so the offline fallback never ran — await both sides.
return (await caches.match(request)) || (await caches.match('/index.html'));
}
}
@@ -70,7 +74,7 @@ async function staleWhileRevalidate(request) {
const networkPromise = fetch(request)
.then((response) => {
cache.put(request, response.clone()).catch(() => {});
if (response && response.ok) cache.put(request, response.clone()).catch(() => {});
return response;
})
.catch(() => null);
@@ -82,8 +86,13 @@ async function staleWhileRevalidate(request) {
self.addEventListener('install', (event) => {
self.skipWaiting();
event.waitUntil(
// DC-170: per-URL caching instead of one all-or-nothing addAll — a single
// renamed/missing asset previously rejected the whole install, leaving the
// OLD service worker (and its stale cache) in control indefinitely.
caches.open(CACHE).then((cache) =>
cache.addAll(PRECACHE.map((url) => new Request(url, { cache: 'reload' })))
Promise.all(PRECACHE.map((url) =>
cache.add(new Request(url, { cache: 'reload' })).catch(() => {})
))
)
);
});