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_PREFER_AUSTIN=1 # prioritize Austin cameras in the registry
# 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
- `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
known Overpass rate limits, timeouts, and query failures. Search feedback no
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"
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_PREFER_AUSTIN="${CCTV_PREFER_AUSTIN:-1}"
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);
}
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 dLon = toRad(lon2 - lon1);
const h = Math.sin(dLat / 2) ** 2
@@ -147,7 +148,7 @@ function buildEntries(pack, kind) {
polygons,
areaKm2,
bbox: [minLon, minLat, maxLon, maxLat],
bboxDiagonalKm: haversineKm(minLon, minLat, maxLon, maxLat),
bboxDiagonalKm: haversineKm(minLat, minLon, maxLat, maxLon),
});
}
return out;
+9 -4
View File
@@ -1645,7 +1645,8 @@ function celestrakProxy() {
send(502, 'celestrak fetch failed and no cache available', 'NONE');
}
} 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');
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(
res,
Number.isInteger(error?.upstreamStatus) ? error.upstreamStatus : 502,
error?.upstreamBody || JSON.stringify({ error: 'Launch Library 2 unavailable' }),
JSON.stringify({ error: 'Launch Library 2 unavailable' }),
'NONE',
);
}
@@ -2373,7 +2376,8 @@ function terrainHeightsProxy() {
}
send(outcome.status, outcome.body);
} 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' });
} 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' });
}
});
},