fix: sanitize proxy error payloads, loopback dev-cctv, drop doc drift

Four small, independently-verified issues from the audit backlog:

- #262/#263: the celestrak, terrain-heights, adsbdb and launch-library
  proxies put raw `err.message` / raw upstream error bodies into HTTP
  responses, contradicting the sanitized-error property KNOWN-ISSUES.md
  documents. They now log the real error server-side (matching the GBFS
  and CCTV proxy convention) and return a generic message.
- #265: scripts/dev-cctv.sh defaulted to HOST=0.0.0.0 while dev-fresh.sh
  and dev-secure.sh default to loopback. Now loopback, opt in with
  HOST=0.0.0.0.
- #274: naturalEarthRegions.js's internal haversineKm took (lon, lat,
  lon, lat) while every other haversineKm in src/ takes (lat, lon, lat,
  lon). Same name, same signature shape, opposite order — a silent
  wrong-distance landmine. Reordered to match; the one call site was
  updated, so behavior is unchanged.
- #270: removed CCTV_AUTO_CALIBRATE and CCTV_DRAPE_MESH from
  .env.example; no code anywhere reads them.

Not done: deduplicating the ~10 haversine implementations into one
shared utility. That is issue #188's scope and a far larger diff;
removing the argument-order landmine is what makes the current state
safe.

Verified: npm test (2705 pass / 0 fail), npm run build green.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01C3eGYgDYBo2BCUeDGwGvrp
This commit is contained in:
Tom-NeverwinterandClaude Opus 5 committed 2026-09-10 15:24:57 -05:00
1 parent ea344c952f
commit 0584a1d71d
5 files changed
+24 -9

No files matched your search

-2
View File
@@ -138,5 +138,3 @@ AISSTREAM_API_KEY=
# CCTV_MAX_SOURCES=48 # overall cap on registered cameras # CCTV_MAX_SOURCES=48 # overall cap on registered cameras
# CCTV_PREFER_AUSTIN=1 # prioritize Austin cameras in the registry # CCTV_PREFER_AUSTIN=1 # prioritize Austin cameras in the registry
# CCTV_FORCE_AUSTIN= # force Austin-only sources # CCTV_FORCE_AUSTIN= # force Austin-only sources
# CCTV_AUTO_CALIBRATE=1 # auto-calibrate camera projection (client flag)
# CCTV_DRAPE_MESH=1 # drape frames onto 3D geometry (client flag)
+10
View File
@@ -7,6 +7,16 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
### Fixed ### Fixed
- `scripts/dev-cctv.sh` now binds to `localhost` by default, matching
`dev-fresh.sh` and `dev-secure.sh`. Set `HOST=0.0.0.0` to opt in to LAN
exposure. (#265)
- The Celestrak, terrain-heights, adsbdb, and launch-library proxies no longer
place raw exception text or upstream error bodies in HTTP responses; the real
error is logged server-side and the client gets a generic message. (#262, #263)
- `naturalEarthRegions.js`’s internal `haversineKm` takes `(lat, lon, lat, lon)`
like every sibling implementation instead of longitude-first. (#274)
- Dropped `CCTV_AUTO_CALIBRATE` and `CCTV_DRAPE_MESH` from `.env.example`; no
code reads them. (#270)
- Mapped-site outages show their scheduled retry countdown and distinguish - Mapped-site outages show their scheduled retry countdown and distinguish
known Overpass rate limits, timeouts, and query failures. Search feedback no known Overpass rate limits, timeouts, and query failures. Search feedback no
longer claims a refresh succeeded while the layer is unavailable or loading. longer claims a refresh succeeded while the layer is unavailable or loading.
+2 -1
View File
@@ -5,7 +5,8 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR" cd "$ROOT_DIR"
PORT="${PORT:-4173}" PORT="${PORT:-4173}"
HOST="${HOST:-0.0.0.0}" # Local-only by default; set HOST=0.0.0.0 explicitly to expose on the LAN.
HOST="${HOST:-localhost}"
CCTV_SOURCES_FILE="${CCTV_SOURCES_FILE:-config/cctv_sources.austin.json}" CCTV_SOURCES_FILE="${CCTV_SOURCES_FILE:-config/cctv_sources.austin.json}"
CCTV_PREFER_AUSTIN="${CCTV_PREFER_AUSTIN:-1}" CCTV_PREFER_AUSTIN="${CCTV_PREFER_AUSTIN:-1}"
CCTV_AUSTIN_MAX_SOURCES="${CCTV_AUSTIN_MAX_SOURCES:-36}" CCTV_AUSTIN_MAX_SOURCES="${CCTV_AUSTIN_MAX_SOURCES:-36}"
+3 -2
View File
@@ -34,7 +34,8 @@ function ringAreaKm2(ring) {
return Math.abs((sum * EARTH_RADIUS_KM * EARTH_RADIUS_KM) / 2); return Math.abs((sum * EARTH_RADIUS_KM * EARTH_RADIUS_KM) / 2);
} }
function haversineKm(lon1, lat1, lon2, lat2) { /** Great-circle distance in km. Arg order matches every sibling: (lat, lon, lat, lon). */
function haversineKm(lat1, lon1, lat2, lon2) {
const dLat = toRad(lat2 - lat1); const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1); const dLon = toRad(lon2 - lon1);
const h = Math.sin(dLat / 2) ** 2 const h = Math.sin(dLat / 2) ** 2
@@ -147,7 +148,7 @@ function buildEntries(pack, kind) {
polygons, polygons,
areaKm2, areaKm2,
bbox: [minLon, minLat, maxLon, maxLat], bbox: [minLon, minLat, maxLon, maxLat],
bboxDiagonalKm: haversineKm(minLon, minLat, maxLon, maxLat), bboxDiagonalKm: haversineKm(minLat, minLon, maxLat, maxLon),
}); });
} }
return out; return out;
+9 -4
View File
@@ -1645,7 +1645,8 @@ function celestrakProxy() {
send(502, 'celestrak fetch failed and no cache available', 'NONE'); send(502, 'celestrak fetch failed and no cache available', 'NONE');
} }
} catch (err) { } catch (err) {
send(500, `celestrak proxy error: ${err?.message || err}`, 'ERROR'); console.error('[celestrak-proxy]', err?.message || String(err));
send(500, 'celestrak proxy error', 'ERROR');
} }
}); });
}, },
@@ -1755,10 +1756,12 @@ function rocketLaunchesProxy() {
send(res, 200, stale.body, 'STALE-ERROR'); send(res, 200, stale.body, 'STALE-ERROR');
return; return;
} }
// Upstream error bodies stay server-side; the client gets a generic message.
if (error?.upstreamBody) console.warn('[launch-library-proxy] upstream error body:', error.upstreamBody);
send( send(
res, res,
Number.isInteger(error?.upstreamStatus) ? error.upstreamStatus : 502, Number.isInteger(error?.upstreamStatus) ? error.upstreamStatus : 502,
error?.upstreamBody || JSON.stringify({ error: 'Launch Library 2 unavailable' }), JSON.stringify({ error: 'Launch Library 2 unavailable' }),
'NONE', 'NONE',
); );
} }
@@ -2373,7 +2376,8 @@ function terrainHeightsProxy() {
} }
send(outcome.status, outcome.body); send(outcome.status, outcome.body);
} catch (err) { } catch (err) {
send(500, { error: `terrain heights proxy error: ${err?.message || err}` }); console.error('[terrain-heights-proxy]', err?.message || String(err));
send(500, { error: 'terrain heights proxy error' });
} }
}); });
}, },
@@ -2494,7 +2498,8 @@ function adsbdbProxy() {
} }
return send(404, { error: 'unknown endpoint' }); return send(404, { error: 'unknown endpoint' });
} catch (err) { } catch (err) {
return send(500, { error: String(err?.message || err) }); console.error('[adsbdb-proxy]', err?.message || String(err));
return send(500, { error: 'adsbdb proxy error' });
} }
}); });
}, },