From d04939b813da2e9d5f4a0bc3e056616d78a57c90 Mon Sep 17 00:00:00 2001 From: Gaurav Patidar Date: Wed, 9 Sep 2026 13:23:59 +0530 Subject: [PATCH 01/12] fix:docs --- CONTRIBUTING.md | 19 +++++++++++++++++++ DATA_SOURCES.md | 30 ++++++++++++++++++++++++++++++ docs/KNOWN-ISSUES.md | 6 +++--- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6760dfdf1..029b53514 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,6 +27,25 @@ through Pinokio; the terminal path above remains the contributor path. Open `http://localhost:4173`. Before sending a PR run `npm run build`, `npm test`, and `npm run test:track` (dev server must be up) — **all three must stay green.** +### Test prerequisites + +- **`npm test`** — unit tests. Runs standalone; no dev server or credentials + required. Exit code is the gate. +- **`npm run test:track`** — the 3D flight-tracking regression harness. It + drives the **real app** in headless Chromium, so it **requires a running + dev server** on the default port (`http://localhost:4173`). Pass + `--url ` to point it at another instance (e.g. + `node scripts/track-regression.mjs --url http://localhost:4272`). It does not + depend on live OpenSky / adsb.lol / AISStream (those are optional or + rate-limited locally); it installs a persistent `fetch` shim that returns + synthetic aircraft in the exact upstream payload shapes the layers parse. +- **`npm run build`** — production Vite build. No server or credentials + required; it validates that the client bundle compiles cleanly. + +If you change runtime behavior, update `docs/CURRENT-STATE.md` and +`CHANGELOG.md` in the same PR. For a deeper catalog of what each harness +checks and what it needs to run, see [TESTING.md](TESTING.md). + ## Good first contributions The highest-leverage places to jump in: diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index faa351dca..bb0626117 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -125,6 +125,36 @@ the retrieval date (2026-07-30), exact download URL, license evidence, and the deterministic transform (`scripts/build-sf-neighborhoods.mjs`: `nhood` → `name`, ~2 m Douglas-Peucker simplification, 6-decimal rounding). +### Pilot CCTV source packs (`config/`) + +`config/cctv_sources.shinjuku.json` is a **demo source pack** (3 cameras: +Shinjuku Crossing, Shinjuku Station West, Shibuya Scramble) used for +projection-pipeline testing. The feeds are Google-hosted sample MP4 videos +(`storage.googleapis.com/gtv-videos-bucket/sample/`), not live public cameras. +Camera poses (coordinates, heading, pitch, FOV, mount height) are fabricated +priors placed at recognizable Tokyo intersections. + +Use the pack by setting: + +```bash +CCTV_SOURCES_FILE=config/cctv_sources.shinjuku.json npm run dev +``` + +Or equivalently, via `./scripts/dev-cctv.sh`: + +```bash +CCTV_SOURCES_FILE=config/cctv_sources.shinjuku.json ./scripts/dev-cctv.sh +``` + +The pack exercises the same CCTV registration and frustum-projection pipeline +as live city packs without requiring network access to a camera catalog. It is +**not** a live data source and carries no external attribution requirement. The +sample videos are publicly available Google test media. + +`config/cctv_sources.austin.json` is the reference live pack. Custom source +packs follow the same JSON schema (see either file); `CCTV_SOURCES_JSON` +accepts inline JSON as an alternative to a file path. + --- ## In-app attribution diff --git a/docs/KNOWN-ISSUES.md b/docs/KNOWN-ISSUES.md index 33c665f39..ca46d2ef0 100644 --- a/docs/KNOWN-ISSUES.md +++ b/docs/KNOWN-ISSUES.md @@ -57,9 +57,9 @@ Status: Open (owner-accepted 2026-07-08, documented) a few stragglers take one more poll. - **Born-grounded first poll:** a contact first seen on the ground with no altitude data renders at the geoid for ≤1 poll until its floor cell warms. -- Full context, improvement ideas, and the verification oracle - (`scripts/qa-floor-verify.mjs`): - `docs/superpowers/reports/2026-07-08-height-datum-handover.md`. +- Verify floor-placement behavior with `scripts/qa-floor-verify.mjs` against a + running local app; set `QA_BASE_URL` when it is not on the default port + (`http://localhost:4173`). --- From e3772f806f660ade51817707208250c7b7dd9a51 Mon Sep 17 00:00:00 2001 From: Gaurav Patidar Date: Fri, 11 Sep 2026 00:55:57 +0530 Subject: [PATCH 02/12] Clarify test:track command usage in CONTRIBUTING.md Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CONTRIBUTING.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 029b53514..eef422a84 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,9 +33,8 @@ Open `http://localhost:4173`. Before sending a PR run `npm run build`, `npm test required. Exit code is the gate. - **`npm run test:track`** — the 3D flight-tracking regression harness. It drives the **real app** in headless Chromium, so it **requires a running - dev server** on the default port (`http://localhost:4173`). Pass - `--url ` to point it at another instance (e.g. - `node scripts/track-regression.mjs --url http://localhost:4272`). It does not + dev server** (default `http://localhost:4173`). To target another instance, + run `npm run test:track -- --url ` (or `node scripts/track-regression.mjs --url `). depend on live OpenSky / adsb.lol / AISStream (those are optional or rate-limited locally); it installs a persistent `fetch` shim that returns synthetic aircraft in the exact upstream payload shapes the layers parse. From accb8983ca0fd55eeb0c475612d1cfa5457b911d Mon Sep 17 00:00:00 2001 From: Sunil56224972 Date: Tue, 27 Oct 2026 18:02:32 +0530 Subject: [PATCH 03/12] fix(security): add cross-site gate to military-installations proxy The /api/military-installations endpoint was the last keyless proxy without the admitSameSite gate that protects terrain, CCTV, traffic, places, and Celestrak. A cross-site , foreign Origin, opaque origin, or reverse-proxy-forwarded request now receives 403 before any Overpass query. Same-origin app requests and bare tool requests continue to work. Adds 4 integration tests (cross-site refused, same-origin admitted, bare tool admitted, proxied refused) following the terrainHeightsBounds test pattern. --- ...-military-installations-cross-site-gate.md | 6 + pr-body.md | 75 +++++++++ scripts/package-boundaries.json | 4 +- server/providers/military-installations.js | 2 + src/militaryInstallationsGate.test.mjs | 147 ++++++++++++++++++ 5 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 changelog.d/1427-military-installations-cross-site-gate.md create mode 100644 pr-body.md create mode 100644 src/militaryInstallationsGate.test.mjs diff --git a/changelog.d/1427-military-installations-cross-site-gate.md b/changelog.d/1427-military-installations-cross-site-gate.md new file mode 100644 index 000000000..3665da022 --- /dev/null +++ b/changelog.d/1427-military-installations-cross-site-gate.md @@ -0,0 +1,6 @@ +- The military-installations proxy (`/api/military-installations`) now refuses + cross-site browser requests with the same `admitSameSite` gate that protects + the terrain, CCTV, traffic, places, and Celestrak proxies. A cross-site + ``, a foreign `Origin`, an opaque origin, or a reverse-proxy-forwarded + request receives 403 before any Overpass query runs. Same-origin app requests + and bare tool requests (curl, Node harness) continue to work. (Sunil56224972) diff --git a/pr-body.md b/pr-body.md new file mode 100644 index 000000000..28e73decc --- /dev/null +++ b/pr-body.md @@ -0,0 +1,75 @@ +## Summary + +Adds the `admitSameSite` cross-site gate to `/api/military-installations`, as offered in the [#975 merge comment](https://github.com/bilawalsidhu/gods-eye-view/pull/975#issuecomment-6357981946). + +This was the last keyless proxy endpoint without the gate. After this change, every proxy that sends queries upstream now refuses cross-site browser requests before doing any work. + +| Endpoint | Gate before this PR | Gate after | +|----------|-------------------|------------| +| `/api/terrain/heights` | ✅ admitSameSite (#975) | ✅ | +| `/api/cctv/frame/:id` | ✅ admitSameSite (#922) | ✅ | +| `/api/traffic/*` | ✅ admitSameSite | ✅ | +| `/api/google/*` | ✅ admitSameSite | ✅ | +| `/api/gemini/*` | ✅ admitSameSite | ✅ | +| `/api/space/satellites` | ✅ admitSameSite | ✅ | +| `/api/overpass` | ✅ admitSameSite | ✅ | +| `/api/military-installations` | ❌ **none** | ✅ admitSameSite | + +## The problem + +A cross-site page (or an `` tag from any website) can query `/api/military-installations` and trigger upstream Overpass requests on the user's behalf. The endpoint has a rate limiter (90/min), but no cross-site gate — so a hostile page can burn through the user's Overpass quota and exfiltrate the response data. + +Every other proxy in the codebase already refuses these shapes. Military-installations was the gap. + +## The fix + +Two lines in `server/providers/military-installations.js`: + +```diff + import { makeRateLimiter, clientKey } from './common/rate-limit.js'; ++import { admitSameSite } from './common/same-site.js'; +``` + +```diff + if (req.method !== 'GET') { + res.writeHead(405, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Method Not Allowed' })); + return; + } ++ if (admitSameSite(req, res)) return; + const url = new URL(req.url, 'http://localhost'); +``` + +Same pattern as terrain.js, cctv.js, traffic.js, google.js, etc. Gate runs before the rate limiter, so refused requests never consume quota. + +`scripts/package-boundaries.json` updated to declare `server/providers/common/same-site.js` and `src/localRequestGate.mjs` as owned modules of `military-installations-provider`. + +## Tests + +New file: `src/militaryInstallationsGate.test.mjs` — 4 integration tests following the `terrainHeightsBounds.test.mjs` pattern: + +``` +✔ cross-site browser requests are refused with 403 (2.4ms) +✔ same-origin browser request is admitted (not 403) (5.3ms) +✔ bare tool request (no Origin, no Sec-Fetch) is admitted (0.9ms) +✔ proxied same-origin requests are refused (proxy headers present) (0.5ms) +``` + +Tested shapes: +- `Sec-Fetch-Site: cross-site` (the `` case) → 403 +- `Sec-Fetch-Site: same-site` (cross-origin sibling) → 403 +- `Origin: https://evil.example` (fetch from hostile page) → 403 +- `Origin: null` (sandboxed iframe) → 403 +- `X-Forwarded-For` + `Sec-Fetch-Site: cross-site` (proxied cross-site) → 403 +- Same-origin with matching `Origin` → admitted +- No `Origin`, no `Sec-Fetch` (curl / Node) → admitted +- Proxy headers with `same-origin` → 403 + +All 4 tests fail on `main` and pass with the fix. + +## Validation + +- `node --test src/localRequestGate.test.mjs src/militaryInstallationsGate.test.mjs`: **18 tests, 18 pass, 0 fail** +- `npm run check:boundaries`: **pass** (military-installations-provider: 15 owned modules) +- `npm run check:import-directions`: pass +- Changelog fragment in `changelog.d/military-installations-cross-site-gate.md` diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index b42d90fdc..4219b311c 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -298,7 +298,9 @@ "server/providers/overpass/transport.js", "src/sources/httpBody.js", "src/i18n/marker.js", - "src/sources/rateLimit.js" + "src/sources/rateLimit.js", + "server/providers/common/same-site.js", + "src/localRequestGate.mjs" ], "external": [] }, diff --git a/server/providers/military-installations.js b/server/providers/military-installations.js index 7d252b8ed..1b6c82107 100644 --- a/server/providers/military-installations.js +++ b/server/providers/military-installations.js @@ -1,4 +1,5 @@ import { makeRateLimiter, clientKey } from './common/rate-limit.js'; +import { admitSameSite } from './common/same-site.js'; import { MILITARY_INSTALLATION_ELEMENT_CAP, MILITARY_INSTALLATION_MAX_RESPONSE_BYTES, @@ -97,6 +98,7 @@ function militaryInstallationsProxy() { res.end(JSON.stringify({ error: 'Method Not Allowed' })); return; } + if (admitSameSite(req, res)) return; const url = new URL(req.url, 'http://localhost'); const requested = validMilitaryInstallationBox(url.searchParams); if (!requested) { diff --git a/src/militaryInstallationsGate.test.mjs b/src/militaryInstallationsGate.test.mjs new file mode 100644 index 000000000..4451c384b --- /dev/null +++ b/src/militaryInstallationsGate.test.mjs @@ -0,0 +1,147 @@ +// Military-installations cross-site gate — verifies that the admitSameSite +// gate added in this PR actually refuses the same request shapes the terrain +// and CCTV proxies already refuse, and admits same-origin and bare-tool +// requests. +// +// Run with: npm test +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { militaryInstallationsProxy } from '../server/providers/military-installations.js'; + +function install(plugin) { + const routes = new Map(); + plugin.configureServer({ + middlewares: { + use(path, handler) { + routes.set(path, handler); + }, + }, + }); + return routes; +} + +function request( + handler, + { url = '/', method = 'GET', remoteAddress = '127.0.0.1', reqHeaders = {} } = {}, +) { + return new Promise((resolve, reject) => { + const headers = new Map(); + const req = { + method, + url, + headers: reqHeaders, + socket: { remoteAddress }, + }; + const res = { + statusCode: 200, + headersSent: false, + setHeader(name, value) { + headers.set(String(name).toLowerCase(), String(value)); + }, + writeHead(status, hdrs = {}) { + this.statusCode = status; + this.headersSent = true; + for (const [name, value] of Object.entries(hdrs)) + headers.set(String(name).toLowerCase(), String(value)); + }, + end(body = '') { + let parsed = null; + try { + parsed = body ? JSON.parse(String(body)) : null; + } catch { + parsed = String(body); + } + resolve({ + statusCode: this.statusCode, + headers: Object.fromEntries(headers), + body: parsed, + }); + }, + }; + Promise.resolve(handler(req, res)).catch(reject); + }); +} + +// A valid bbox query string for military-installations. +const VALID_BBOX = '?south=48.8&west=2.2&north=48.9&east=2.4'; +const HOST = 'localhost:5173'; + +test('cross-site browser requests are refused with 403', async () => { + const route = install(militaryInstallationsProxy()).get( + '/api/military-installations', + ); + const refused = [ + // from any web page + { host: HOST, 'sec-fetch-site': 'cross-site' }, + // same-site but cross-origin sibling + { host: HOST, 'sec-fetch-site': 'same-site' }, + // fetch from another page + { host: HOST, origin: 'https://evil.example' }, + // sandboxed frame + { host: HOST, origin: 'null' }, + // behind a reverse proxy with cross-site + { host: HOST, 'x-forwarded-for': '203.0.113.9', 'sec-fetch-site': 'cross-site' }, + ]; + for (const reqHeaders of refused) { + const answer = await request(route, { url: VALID_BBOX, reqHeaders }); + assert.equal( + answer.statusCode, + 403, + `expected 403 for headers ${JSON.stringify(reqHeaders)}, got ${answer.statusCode}`, + ); + } +}); + +test('same-origin browser request is admitted (not 403)', async () => { + const route = install(militaryInstallationsProxy()).get( + '/api/military-installations', + ); + // Same-origin with matching origin header + const answer = await request(route, { + url: VALID_BBOX, + reqHeaders: { + host: HOST, + origin: `http://${HOST}`, + 'sec-fetch-site': 'same-origin', + }, + }); + // Should get past the gate — may be 200, 429, or 503 depending on Overpass config, + // but never 403. + assert.notEqual(answer.statusCode, 403, 'same-origin request should not be refused'); +}); + +test('bare tool request (no Origin, no Sec-Fetch) is admitted', async () => { + const route = install(militaryInstallationsProxy()).get( + '/api/military-installations', + ); + // curl / Node harness — no browser headers at all + const answer = await request(route, { + url: VALID_BBOX, + reqHeaders: { host: HOST }, + }); + assert.notEqual(answer.statusCode, 403, 'bare tool request should not be refused'); +}); + +test('proxied same-origin requests are refused (proxy headers present)', async () => { + const route = install(militaryInstallationsProxy()).get( + '/api/military-installations', + ); + const proxied = [ + { host: HOST, 'sec-fetch-site': 'same-origin', 'x-forwarded-for': '203.0.113.9' }, + { host: HOST, 'sec-fetch-site': 'same-origin', 'cf-connecting-ip': '203.0.113.9' }, + { + host: HOST, + 'sec-fetch-site': 'same-origin', + forwarded: 'for=203.0.113.9;proto=https', + via: '1.1 caddy', + }, + ]; + for (const reqHeaders of proxied) { + const answer = await request(route, { url: VALID_BBOX, reqHeaders }); + assert.equal( + answer.statusCode, + 403, + `expected 403 for proxied headers ${JSON.stringify(reqHeaders)}, got ${answer.statusCode}`, + ); + } +}); From d8358048adafa95930c1be2db9b624627f51e332 Mon Sep 17 00:00:00 2001 From: Denis Groshev Date: Wed, 28 Oct 2026 16:07:48 +0100 Subject: [PATCH 04/12] fix(dock): show only icons on the narrow dock wings so labels stop covering the voice control At 900px and narrower the Location and Visual Presets wings shrink to --dock-side-compact (4.25rem), open or closed, but their text labels kept their full width. Between 721px and 900px, where the desktop dock is still shown, LOCATION and VISUAL PRESETS spilled over the idle voice control, and the LOCATION label took taps on the left half of the mic button. The label clip that existed was scoped to 620px, which the phone shell (720px) always covers, so it never applied to a visible dock. In that band the labels are now font-size 0 and lose their chevrons, so each wing shows only its icon. The label text is unchanged, so the toggles' "Expand/Collapse ..." aria-label and title, which panelChrome builds from it, keep the full name. The Cyber HUD layout sets a wider --dock-side-compact that fits its labels, so it keeps them. The unreachable 620px clip is removed. Found in GEV launch-readiness QA (HAL-728). --- docs/CURRENT-STATE.md | 14 +++++++++++ src/ui/styles/command-dock-trays.css | 35 ++++++++++++++++++++++------ 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 3e7f333a1..cfa9be32f 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -2665,6 +2665,20 @@ isCurrent })` gates BOTH halves of the map-stack switch, which is its only > name the panel (`#location-bar` / `#control-panel`) to reach (2,5,0). Adding > a new tray rule means checking it against the pinned variants, not just > against the base rule. +> - **Dock wings are icon-only from 900px down to 721px (2026-10-28):** at +> ≤900px `--dock-side-compact` shrinks the Location and Visual Presets wings +> to 4.25rem, open or closed (an open tray pops out above the dock), which is +> too narrow for their text labels. The labels used to spill over the voice +> control, and the LOCATION label captured taps on the left half of the mic +> button (HAL-728). `src/ui/styles/command-dock-trays.css` now sets the +> labels to `font-size: 0` and drops their `›`/`‹` chevrons in that band, so +> each wing shows only its icon. The text node is unchanged, so +> `panelChrome.js` still builds the toggles' "Expand/Collapse …" `aria-label` +> and `title` from it; the browser tooltip is the only visible name on hover. +> The Cyber HUD layout is excluded: it sets `--dock-side-compact` to +> 9–9.75rem, which fits its labels. The former ≤620px ellipsis clip is gone: +> the phone shell takes over at 720px, so it never applied to a visible +> dock. > - **LOCATION mini-status is data-only today (2026-08-20):** the collapsed > readout now follows a free-text geocode search as well as preset pills > (`src/locationStatus.js` owns the copy for both), and every other camera diff --git a/src/ui/styles/command-dock-trays.css b/src/ui/styles/command-dock-trays.css index 547037de7..5575b588a 100644 --- a/src/ui/styles/command-dock-trays.css +++ b/src/ui/styles/command-dock-trays.css @@ -502,6 +502,34 @@ body:not(.ui-clean-view):not(.recording-mode):has( #command-dock .gev-voice-error-tray { width: min(24rem, calc(100vw - 1.5rem)); } + + /* The wings shrink to --dock-side-compact at this width, open or closed (an + open tray pops out above the dock), which is too narrow for the text + labels; they would spill over the voice control and take its taps. The + wings show only their icon; the toggles keep their aria-label. Cyber sets + a wider --dock-side-compact that fits its labels, so it keeps them. */ + :root:not([data-ui-theme='cyber']) + #command-dock + :is(#location-bar, #control-panel) + .panel-title, + :root:not([data-ui-theme='cyber']) + #command-dock + :is(#location-bar, #control-panel) + .location-toolbar-label { + font-size: 0; + letter-spacing: 0; + } + + :root:not([data-ui-theme='cyber']) + #command-dock + :is(#location-bar, #control-panel) + .panel-title::after, + :root:not([data-ui-theme='cyber']) + #command-dock + :is(#location-bar, #control-panel) + .location-toolbar-label::after { + content: none; + } } @media (max-width: 620px) { @@ -510,13 +538,6 @@ body:not(.ui-clean-view):not(.recording-mode):has( --dock-voice-width: 8.75rem; } - #command-dock .panel-title, - #command-dock .location-toolbar-label { - max-width: 2.3rem; - overflow: hidden; - text-overflow: ellipsis; - } - #left-panel-stack { bottom: calc(50vh + 8px); } From ce10a46b7f1780fcdeadfaf5f47396ce79065393 Mon Sep 17 00:00:00 2001 From: Denis Groshev Date: Wed, 28 Oct 2026 20:22:01 +0100 Subject: [PATCH 05/12] docs(changelog): add the dock icon-only wings entry --- changelog.d/1457-dock-icon-only-wings.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelog.d/1457-dock-icon-only-wings.md diff --git a/changelog.d/1457-dock-icon-only-wings.md b/changelog.d/1457-dock-icon-only-wings.md new file mode 100644 index 000000000..21825669b --- /dev/null +++ b/changelog.d/1457-dock-icon-only-wings.md @@ -0,0 +1,5 @@ +- Between 721px and 900px wide (tablet portrait, phone landscape), the + Location and Visual Presets dock wings show only their icons, so their + labels no longer spill over the voice control or take taps meant for the + mic button. The full name stays in the tooltip and accessible name, and the + Cyber layout keeps its labels (ZloyS, #1457). From 2a335a8f3560ab16bc4fe5cd4956939def32ad46 Mon Sep 17 00:00:00 2001 From: Luis Ortiz <2839770+anobjectn@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:34:04 -0400 Subject: [PATCH 06/12] fix(deps): pin Node 24.22.1 for the October 21, 2026 security release Node.js 24.14.0 carries CVE-2026-111497 ("Node2Shell"), an out-of-bounds write in the bundled Brotli decoder that can lead to remote code execution, plus several high-severity DNS, TLS and URL-parsing fixes. 24.22.1 is the first patched 24.x release. - Dockerfile: node:24.22.1-slim, digest re-pinned to the multi-arch index - CI: 24.22.1 in the verify matrix and the other two jobs; the allocation-gate requirement now keys on 24.22.1 - Docs: contributor setup and the dependency baseline name 24.22.1 The engines floor and the setup doctor are unchanged. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 8 ++++---- CONTRIBUTING.md | 8 +++++--- Dockerfile | 2 +- README.md | 5 +++-- docs/CURRENT-STATE.md | 4 +++- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce3563685..766f0cc51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,10 +20,10 @@ jobs: strategy: fail-fast: false matrix: - node: [24.14.0, 26.x] + node: [24.22.1, 26.x] env: PUPPETEER_SKIP_DOWNLOAD: '1' - GEV_REQUIRE_ALLOCATION_GATE: ${{ matrix.node == '24.14.0' && '1' || '0' }} + GEV_REQUIRE_ALLOCATION_GATE: ${{ matrix.node == '24.22.1' && '1' || '0' }} steps: - name: Check out source uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 @@ -76,7 +76,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - node-version: 24.14.0 + node-version: 24.22.1 cache: npm - name: Install locked dependencies and headless Chrome @@ -131,7 +131,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - node-version: 24.14.0 + node-version: 24.22.1 cache: npm - name: Run the Pinokio install path diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a7fe8ab76..591cdd85d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,13 +4,15 @@ Thanks for being here. God's Eye View is an open foundation for live spatial int ## Getting set up -Use Node.js 24.14.x or 26.x (also enforced by `package.json`). +Use Node.js 24.14.x or 26.x (also enforced by `package.json`). Prefer 24.22.1 or +later: earlier releases lack the October 21, 2026 security fixes, including +CVE-2026-111497 (a Brotli decoder flaw that can lead to remote code execution). ```bash git clone https://github.com/bilawalsidhu/gods-eye-view.git cd gods-eye-view -nvm install 24.14.0 -nvm use 24.14.0 +nvm install 24.22.1 +nvm use 24.22.1 npm install npm run doctor ./scripts/dev-fresh.sh # or: npm run dev (keys are optional) diff --git a/Dockerfile b/Dockerfile index 433d170a8..e711ec1d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ # proxies (ADS-B, AIS, Overpass, OpenAI Realtime, ...) as middleware, so there # is nothing else to containerise. See compose.yaml for the usual entry point. # Same Node as CI (package.json engines, .github/workflows), pinned by digest. -FROM node:24.14.0-slim@sha256:d8e448a56fc63242f70026718378bd4b00f8c82e78d20eefb199224a4d8e33d8 +FROM node:24.22.1-slim@sha256:be8204d4bd327363357f532187b752ff62759c31bedf98e9e18fc08c9cbd605c # Puppeteer is only used by the QA scripts; skip its ~150MB Chromium download. ENV PUPPETEER_SKIP_DOWNLOAD=1 diff --git a/README.md b/README.md index 0a7509efa..ee9c7819a 100644 --- a/README.md +++ b/README.md @@ -129,8 +129,9 @@ hardware; it does not establish a minimum GPU requirement. ### Path 2 — Terminal / coding agent -Use **Node.js 24.x (24.14.0 or later) or 26.x**. The setup doctor warns about -Node 25, which is end-of-life. +Use **Node.js 24.x (24.14.0 or later) or 26.x**; 24.22.1 or later (26.12.1 or +later on 26) includes the October 21, 2026 security fixes, among them +CVE-2026-111497. The setup doctor warns about Node 25, which is end-of-life. ```bash git clone https://github.com/bilawalsidhu/gods-eye-view.git diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 3e7f333a1..098bd3e83 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -6024,7 +6024,9 @@ The lockfile uses DOMPurify 3.4.17, protobufjs 8.8.0, PostCSS 8.5.28, source-map-js 1.2.2 and nanoid 3.3.19. Cesium remains on 1.138.0. Browser QA uses Puppeteer 25.10.0; image-processing tools use Sharp 0.35.5. QA scripts await Puppeteer's asynchronous executable-path lookup before testing or passing the path to Chrome. Supported Node versions remain -24.14.x and 26.x. Use `npm ci` to reproduce the checked-in dependency tree. +24.14.x and 26.x; the Docker image and CI pin 24.22.1, the first 24.x release with +the October 21, 2026 Node.js security fixes (including CVE-2026-111497). Use +`npm ci` to reproduce the checked-in dependency tree. ### Traffic city navigation From 17d0877a4cedf1b44b9ce8f0927654ef5436390d Mon Sep 17 00:00:00 2001 From: Luis Ortiz <2839770+anobjectn@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:35:06 -0400 Subject: [PATCH 07/12] docs(changelog): add the #1458 fragment Co-Authored-By: Claude Sonnet 5.5 --- changelog.d/1458-node-24-22-1-security-release.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog.d/1458-node-24-22-1-security-release.md diff --git a/changelog.d/1458-node-24-22-1-security-release.md b/changelog.d/1458-node-24-22-1-security-release.md new file mode 100644 index 000000000..a440892a8 --- /dev/null +++ b/changelog.d/1458-node-24-22-1-security-release.md @@ -0,0 +1,3 @@ +- The Docker image and CI now pin Node 24.22.1, the first 24.x release with + the October 21, 2026 security fixes, including CVE-2026-111497 (a Brotli + decoder flaw that can lead to remote code execution) (anobjectn, #1458). From 16756bb790adb557208e55c2506994e3993cccf8 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Mon, 2 Nov 2026 07:03:49 -0800 Subject: [PATCH 08/12] Align the doctor, state doc and bug template with the 24.22.1 pin, and credit the author --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- CONTRIBUTING.md | 6 +++--- changelog.d/1458-node-24-22-1-security-release.md | 4 ++-- docs/CURRENT-STATE.md | 5 +++-- scripts/setup-doctor.mjs | 2 +- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 016576bf8..9069c648d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -40,7 +40,7 @@ body: attributes: label: OS, Node version, browser description: Include the versions you can find. Write "unknown" for anything unavailable. - placeholder: "Windows 11, Node 24.14.0, Chrome 151" + placeholder: "Windows 11, Node 24.22.1, Chrome 151" validations: required: true - type: input diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 341252ba4..5850fe650 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,9 +4,9 @@ Thanks for being here. God's Eye View is an open foundation for live spatial int ## Getting set up -Use Node.js 24.14.x or 26.x (also enforced by `package.json`). Prefer 24.22.1 or -later: earlier releases lack the October 21, 2026 security fixes, including -CVE-2026-111497 (a Brotli decoder flaw that can lead to remote code execution). +Use Node.js 24 (24.14.0 or later) or 26.x (also enforced by `package.json`). +Prefer 24.22.1 or later: earlier releases lack the October 21, 2026 security +fixes, including CVE-2026-111497 (a Brotli decoder flaw). ```bash git clone https://github.com/bilawalsidhu/gods-eye-view.git diff --git a/changelog.d/1458-node-24-22-1-security-release.md b/changelog.d/1458-node-24-22-1-security-release.md index a440892a8..8c8cdd318 100644 --- a/changelog.d/1458-node-24-22-1-security-release.md +++ b/changelog.d/1458-node-24-22-1-security-release.md @@ -1,3 +1,3 @@ - The Docker image and CI now pin Node 24.22.1, the first 24.x release with - the October 21, 2026 security fixes, including CVE-2026-111497 (a Brotli - decoder flaw that can lead to remote code execution) (anobjectn, #1458). + the October 21, 2026 security fixes, including CVE-2026-111497 in the Brotli + decoder (Luis Ortiz, @anobjectn, #1458). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index c872b609c..bf5d99fc7 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -3433,7 +3433,8 @@ down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with > style-switch persistence state. Browser/GPU performance comparison remains > operator-side; the final accounting records only comparable population > reductions and deterministic Node allocation/solve measurements. Those -> allocation gates are calibrated on Node.js 24.14.x; `package.json` permits +> allocation gates were calibrated on Node.js 24.14.x and run in CI on the +> pinned Node.js 24.22.1 with the same ceilings; `package.json` permits > supported product runtimes on Node 24 or 26, while the allocation runner > separately enforces Node 24 for these two calibrated probes. Each probe > compiles synchronously and discards explicit-GC @@ -6180,7 +6181,7 @@ The lockfile uses DOMPurify 3.4.17, protobufjs 8.8.0, PostCSS 8.5.28, source-map-js 1.2.2 and nanoid 3.3.19. Cesium remains on 1.138.0. Browser QA uses Puppeteer 25.10.0; image-processing tools use Sharp 0.35.5. QA scripts await Puppeteer's asynchronous executable-path lookup before testing or passing the path to Chrome. Supported Node versions remain -24.14.x and 26.x; the Docker image and CI pin 24.22.1, the first 24.x release with +Node 24 (24.14.0 or later) and 26.x; the Docker image and CI pin 24.22.1, the first 24.x release with the October 21, 2026 Node.js security fixes (including CVE-2026-111497). Use `npm ci` to reproduce the checked-in dependency tree. diff --git a/scripts/setup-doctor.mjs b/scripts/setup-doctor.mjs index 7075be2c1..600e96b09 100644 --- a/scripts/setup-doctor.mjs +++ b/scripts/setup-doctor.mjs @@ -57,7 +57,7 @@ export function classifyNodeVersion(version = process.versions.node) { // NEWER than this release has verified is a warning, never a refusal: a // future Node must not brick a no-terminal install with advice its user // cannot follow. Too-old stays an error above — old runtimes genuinely fail. - return { level: 'warn', summary: 'newer than this release has verified; Node 24.14.x or 26.x is the tested path' }; + return { level: 'warn', summary: 'newer than this release has verified; Node 24.22.x or 26.x is the tested path' }; } /** Verify that every direct package declared by this checkout is present. */ From e9c6f2541f10ebc06ffbb9a9885bfc856224c835 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Mon, 2 Nov 2026 07:10:40 -0800 Subject: [PATCH 09/12] Drop the gap beside the icon-only dock wings, point the state doc at the Vue dock, and credit the author --- changelog.d/1457-dock-icon-only-wings.md | 2 +- docs/CURRENT-STATE.md | 8 ++++---- src/ui/styles/command-dock-trays.css | 2 ++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/changelog.d/1457-dock-icon-only-wings.md b/changelog.d/1457-dock-icon-only-wings.md index 21825669b..3d82cb934 100644 --- a/changelog.d/1457-dock-icon-only-wings.md +++ b/changelog.d/1457-dock-icon-only-wings.md @@ -2,4 +2,4 @@ Location and Visual Presets dock wings show only their icons, so their labels no longer spill over the voice control or take taps meant for the mic button. The full name stays in the tooltip and accessible name, and the - Cyber layout keeps its labels (ZloyS, #1457). + Cyber layout keeps its labels (Denis Groshev, @ZloyS, #1457). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index a284cfbe5..c6accd22c 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -2801,11 +2801,11 @@ isCurrent })` gates BOTH halves of the map-stack switch, which is its only > to 4.25rem, open or closed (an open tray pops out above the dock), which is > too narrow for their text labels. The labels used to spill over the voice > control, and the LOCATION label captured taps on the left half of the mic -> button (HAL-728). `src/ui/styles/command-dock-trays.css` now sets the +> button. `src/ui/styles/command-dock-trays.css` now sets the > labels to `font-size: 0` and drops their `›`/`‹` chevrons in that band, so -> each wing shows only its icon. The text node is unchanged, so -> `panelChrome.js` still builds the toggles' "Expand/Collapse …" `aria-label` -> and `title` from it; the browser tooltip is the only visible name on hover. +> each wing shows only its icon. `CommandDockView.vue` still gives the toggles +> their "Expand/Collapse …" `aria-label` and `title` (`trayWords`), so the +> browser tooltip is the only visible name on hover. > The Cyber HUD layout is excluded: it sets `--dock-side-compact` to > 9–9.75rem, which fits its labels. The former ≤620px ellipsis clip is gone: > the phone shell takes over at 720px, so it never applied to a visible diff --git a/src/ui/styles/command-dock-trays.css b/src/ui/styles/command-dock-trays.css index 2f8585f4a..fb41acb8e 100644 --- a/src/ui/styles/command-dock-trays.css +++ b/src/ui/styles/command-dock-trays.css @@ -511,6 +511,8 @@ body:not(.ui-clean-view):not(.recording-mode):has( .location-toolbar-label { font-size: 0; letter-spacing: 0; + /* The empty text is still a flex item; drop the gap it would claim. */ + gap: 0; } :root:not([data-ui-theme='cyber']) From 799273b3017523b02b5e6414a4af13d2e1d198a5 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Mon, 2 Nov 2026 07:06:06 -0800 Subject: [PATCH 10/12] Test the installations gate with the Overpass proxy tests, drop the stray PR body, and correct the fragment --- ...-military-installations-cross-site-gate.md | 10 +- docs/CURRENT-STATE.md | 2 + pr-body.md | 75 --------- src/militaryInstallationsGate.test.mjs | 147 ------------------ src/overpassOffload.test.mjs | 44 +++++- 5 files changed, 47 insertions(+), 231 deletions(-) delete mode 100644 pr-body.md delete mode 100644 src/militaryInstallationsGate.test.mjs diff --git a/changelog.d/1427-military-installations-cross-site-gate.md b/changelog.d/1427-military-installations-cross-site-gate.md index 3665da022..35d9e73e7 100644 --- a/changelog.d/1427-military-installations-cross-site-gate.md +++ b/changelog.d/1427-military-installations-cross-site-gate.md @@ -1,6 +1,4 @@ -- The military-installations proxy (`/api/military-installations`) now refuses - cross-site browser requests with the same `admitSameSite` gate that protects - the terrain, CCTV, traffic, places, and Celestrak proxies. A cross-site - ``, a foreign `Origin`, an opaque origin, or a reverse-proxy-forwarded - request receives 403 before any Overpass query runs. Same-origin app requests - and bare tool requests (curl, Node harness) continue to work. (Sunil56224972) +- The military-installations proxy (`/api/military-installations`) now refuses + cross-site and reverse-proxy-forwarded browser requests with 403 before any + Overpass work, through the same shared gate as `/api/overpass`. Same-origin + app requests and local tools such as curl still work (@Sunil56224972, #1427). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 84d42cb7b..c771dc02a 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -5084,6 +5084,8 @@ repointing `OVERPASS_UPSTREAMS` refetches instead of serving another instance's answers; untagged entries stop serving once an instance is configured. The shared transport guards `/api/overpass` and `/api/military-installations`. +Both query handlers refuse cross-site and forwarded browser requests through +the shared local-request gate, before any rate-limit, cache or upstream work. With no configured upstream, each route serves existing cache data (including expired entries, marked stale with the original cache/retrieval date) without refresh. An annotation miss returns HTTP 503, `OVERPASS_NOT_CONFIGURED`, diff --git a/pr-body.md b/pr-body.md deleted file mode 100644 index 28e73decc..000000000 --- a/pr-body.md +++ /dev/null @@ -1,75 +0,0 @@ -## Summary - -Adds the `admitSameSite` cross-site gate to `/api/military-installations`, as offered in the [#975 merge comment](https://github.com/bilawalsidhu/gods-eye-view/pull/975#issuecomment-6357981946). - -This was the last keyless proxy endpoint without the gate. After this change, every proxy that sends queries upstream now refuses cross-site browser requests before doing any work. - -| Endpoint | Gate before this PR | Gate after | -|----------|-------------------|------------| -| `/api/terrain/heights` | ✅ admitSameSite (#975) | ✅ | -| `/api/cctv/frame/:id` | ✅ admitSameSite (#922) | ✅ | -| `/api/traffic/*` | ✅ admitSameSite | ✅ | -| `/api/google/*` | ✅ admitSameSite | ✅ | -| `/api/gemini/*` | ✅ admitSameSite | ✅ | -| `/api/space/satellites` | ✅ admitSameSite | ✅ | -| `/api/overpass` | ✅ admitSameSite | ✅ | -| `/api/military-installations` | ❌ **none** | ✅ admitSameSite | - -## The problem - -A cross-site page (or an `` tag from any website) can query `/api/military-installations` and trigger upstream Overpass requests on the user's behalf. The endpoint has a rate limiter (90/min), but no cross-site gate — so a hostile page can burn through the user's Overpass quota and exfiltrate the response data. - -Every other proxy in the codebase already refuses these shapes. Military-installations was the gap. - -## The fix - -Two lines in `server/providers/military-installations.js`: - -```diff - import { makeRateLimiter, clientKey } from './common/rate-limit.js'; -+import { admitSameSite } from './common/same-site.js'; -``` - -```diff - if (req.method !== 'GET') { - res.writeHead(405, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'Method Not Allowed' })); - return; - } -+ if (admitSameSite(req, res)) return; - const url = new URL(req.url, 'http://localhost'); -``` - -Same pattern as terrain.js, cctv.js, traffic.js, google.js, etc. Gate runs before the rate limiter, so refused requests never consume quota. - -`scripts/package-boundaries.json` updated to declare `server/providers/common/same-site.js` and `src/localRequestGate.mjs` as owned modules of `military-installations-provider`. - -## Tests - -New file: `src/militaryInstallationsGate.test.mjs` — 4 integration tests following the `terrainHeightsBounds.test.mjs` pattern: - -``` -✔ cross-site browser requests are refused with 403 (2.4ms) -✔ same-origin browser request is admitted (not 403) (5.3ms) -✔ bare tool request (no Origin, no Sec-Fetch) is admitted (0.9ms) -✔ proxied same-origin requests are refused (proxy headers present) (0.5ms) -``` - -Tested shapes: -- `Sec-Fetch-Site: cross-site` (the `` case) → 403 -- `Sec-Fetch-Site: same-site` (cross-origin sibling) → 403 -- `Origin: https://evil.example` (fetch from hostile page) → 403 -- `Origin: null` (sandboxed iframe) → 403 -- `X-Forwarded-For` + `Sec-Fetch-Site: cross-site` (proxied cross-site) → 403 -- Same-origin with matching `Origin` → admitted -- No `Origin`, no `Sec-Fetch` (curl / Node) → admitted -- Proxy headers with `same-origin` → 403 - -All 4 tests fail on `main` and pass with the fix. - -## Validation - -- `node --test src/localRequestGate.test.mjs src/militaryInstallationsGate.test.mjs`: **18 tests, 18 pass, 0 fail** -- `npm run check:boundaries`: **pass** (military-installations-provider: 15 owned modules) -- `npm run check:import-directions`: pass -- Changelog fragment in `changelog.d/military-installations-cross-site-gate.md` diff --git a/src/militaryInstallationsGate.test.mjs b/src/militaryInstallationsGate.test.mjs deleted file mode 100644 index 4451c384b..000000000 --- a/src/militaryInstallationsGate.test.mjs +++ /dev/null @@ -1,147 +0,0 @@ -// Military-installations cross-site gate — verifies that the admitSameSite -// gate added in this PR actually refuses the same request shapes the terrain -// and CCTV proxies already refuse, and admits same-origin and bare-tool -// requests. -// -// Run with: npm test -import { test } from 'node:test'; -import assert from 'node:assert/strict'; -import { militaryInstallationsProxy } from '../server/providers/military-installations.js'; - -function install(plugin) { - const routes = new Map(); - plugin.configureServer({ - middlewares: { - use(path, handler) { - routes.set(path, handler); - }, - }, - }); - return routes; -} - -function request( - handler, - { url = '/', method = 'GET', remoteAddress = '127.0.0.1', reqHeaders = {} } = {}, -) { - return new Promise((resolve, reject) => { - const headers = new Map(); - const req = { - method, - url, - headers: reqHeaders, - socket: { remoteAddress }, - }; - const res = { - statusCode: 200, - headersSent: false, - setHeader(name, value) { - headers.set(String(name).toLowerCase(), String(value)); - }, - writeHead(status, hdrs = {}) { - this.statusCode = status; - this.headersSent = true; - for (const [name, value] of Object.entries(hdrs)) - headers.set(String(name).toLowerCase(), String(value)); - }, - end(body = '') { - let parsed = null; - try { - parsed = body ? JSON.parse(String(body)) : null; - } catch { - parsed = String(body); - } - resolve({ - statusCode: this.statusCode, - headers: Object.fromEntries(headers), - body: parsed, - }); - }, - }; - Promise.resolve(handler(req, res)).catch(reject); - }); -} - -// A valid bbox query string for military-installations. -const VALID_BBOX = '?south=48.8&west=2.2&north=48.9&east=2.4'; -const HOST = 'localhost:5173'; - -test('cross-site browser requests are refused with 403', async () => { - const route = install(militaryInstallationsProxy()).get( - '/api/military-installations', - ); - const refused = [ - // from any web page - { host: HOST, 'sec-fetch-site': 'cross-site' }, - // same-site but cross-origin sibling - { host: HOST, 'sec-fetch-site': 'same-site' }, - // fetch from another page - { host: HOST, origin: 'https://evil.example' }, - // sandboxed frame - { host: HOST, origin: 'null' }, - // behind a reverse proxy with cross-site - { host: HOST, 'x-forwarded-for': '203.0.113.9', 'sec-fetch-site': 'cross-site' }, - ]; - for (const reqHeaders of refused) { - const answer = await request(route, { url: VALID_BBOX, reqHeaders }); - assert.equal( - answer.statusCode, - 403, - `expected 403 for headers ${JSON.stringify(reqHeaders)}, got ${answer.statusCode}`, - ); - } -}); - -test('same-origin browser request is admitted (not 403)', async () => { - const route = install(militaryInstallationsProxy()).get( - '/api/military-installations', - ); - // Same-origin with matching origin header - const answer = await request(route, { - url: VALID_BBOX, - reqHeaders: { - host: HOST, - origin: `http://${HOST}`, - 'sec-fetch-site': 'same-origin', - }, - }); - // Should get past the gate — may be 200, 429, or 503 depending on Overpass config, - // but never 403. - assert.notEqual(answer.statusCode, 403, 'same-origin request should not be refused'); -}); - -test('bare tool request (no Origin, no Sec-Fetch) is admitted', async () => { - const route = install(militaryInstallationsProxy()).get( - '/api/military-installations', - ); - // curl / Node harness — no browser headers at all - const answer = await request(route, { - url: VALID_BBOX, - reqHeaders: { host: HOST }, - }); - assert.notEqual(answer.statusCode, 403, 'bare tool request should not be refused'); -}); - -test('proxied same-origin requests are refused (proxy headers present)', async () => { - const route = install(militaryInstallationsProxy()).get( - '/api/military-installations', - ); - const proxied = [ - { host: HOST, 'sec-fetch-site': 'same-origin', 'x-forwarded-for': '203.0.113.9' }, - { host: HOST, 'sec-fetch-site': 'same-origin', 'cf-connecting-ip': '203.0.113.9' }, - { - host: HOST, - 'sec-fetch-site': 'same-origin', - forwarded: 'for=203.0.113.9;proto=https', - via: '1.1 caddy', - }, - ]; - for (const reqHeaders of proxied) { - const answer = await request(route, { url: VALID_BBOX, reqHeaders }); - assert.equal( - answer.statusCode, - 403, - `expected 403 for proxied headers ${JSON.stringify(reqHeaders)}, got ${answer.statusCode}`, - ); - } -}); diff --git a/src/overpassOffload.test.mjs b/src/overpassOffload.test.mjs index 602bab8e4..7ef6b0c2b 100644 --- a/src/overpassOffload.test.mjs +++ b/src/overpassOffload.test.mjs @@ -54,14 +54,22 @@ async function call(handlers, url, init = {}) { Object.assign(req, { method: init.method || 'GET', url: parsed.pathname.slice(mount.length) + parsed.search || '/', - headers: {}, + headers: init.headers || {}, socket: { remoteAddress: '127.0.0.1' }, }); - let status, headers, body; + let status, body; + let headers = {}; await handlers.get(mount)(req, { + // The cross-site gate answers through statusCode/setHeader, not writeHead. + set statusCode(s) { + status = s; + }, + setHeader(name, value) { + headers[name] = value; + }, writeHead(s, h) { status = s; - headers = h; + headers = { ...headers, ...h }; }, end(b) { body = b; @@ -166,6 +174,36 @@ test('zero egress: every Overpass consumer reaches real default handlers, and re ); }); +test('the installations proxy refuses cross-site and forwarded requests before any Overpass work', async (t) => { + env(t); + t.mock.method(globalThis, 'fetch', () => + assert.fail('unexpected upstream fetch'), + ); + const handlers = routes(militaryInstallationsProxy()); + const url = + '/api/military-installations?south=48.8&west=2.2&north=48.9&east=2.4'; + for (const headers of [ + { 'sec-fetch-site': 'cross-site' }, + { 'sec-fetch-site': 'same-site' }, + { origin: 'https://foreign.example' }, + { origin: 'null' }, + { 'sec-fetch-site': 'same-origin', 'x-forwarded-for': '203.0.113.9' }, + ]) { + const response = await call(handlers, url, { headers }); + assert.equal(response.status, 403, JSON.stringify(headers)); + assert.equal(response.headers.get('cache-control'), 'no-store'); + } + for (const headers of [ + {}, + { origin: 'http://localhost:4173', 'sec-fetch-site': 'same-origin' }, + ]) { + const response = await call(handlers, url, { + headers: { host: 'localhost:4173', ...headers }, + }); + assert.notEqual(response.status, 403, JSON.stringify(headers)); + } +}); + test('an unconfigured server is probed once and never sent a feature query', async (t) => { env(t); t.mock.method(globalThis, 'fetch', () => From c572a28e4484b45009d8ab71eb42e906109a7cff Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Mon, 2 Nov 2026 07:13:44 -0800 Subject: [PATCH 11/12] Mend the test prerequisites, document local CCTV packs without the dead demo feed, and credit the author --- CONTRIBUTING.md | 16 +++++----- DATA_SOURCES.md | 35 +++++---------------- changelog.d/210-contributor-docs-refresh.md | 4 +++ 3 files changed, 20 insertions(+), 35 deletions(-) create mode 100644 changelog.d/210-contributor-docs-refresh.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5e625cd84..49c0b8ce3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,16 +34,18 @@ Open `http://localhost:4173`. Before sending a PR run `npm run build`, `npm test - **`npm run test:track`** — the 3D flight-tracking regression harness. It drives the **real app** in headless Chromium, so it **requires a running dev server** (default `http://localhost:4173`). To target another instance, - run `npm run test:track -- --url ` (or `node scripts/track-regression.mjs --url `). - depend on live OpenSky / adsb.lol / AISStream (those are optional or - rate-limited locally); it installs a persistent `fetch` shim that returns - synthetic aircraft in the exact upstream payload shapes the layers parse. + run `npm run test:track -- --url ` (or + `node scripts/track-regression.mjs --url `). It does not depend on + live OpenSky / adsb.lol / AISStream (those are optional or rate-limited + locally); it installs a persistent `fetch` shim that returns synthetic + aircraft in the exact upstream payload shapes the layers parse. - **`npm run build`** — production Vite build. No server or credentials required; it validates that the client bundle compiles cleanly. -If you change runtime behavior, update `docs/CURRENT-STATE.md` and -`CHANGELOG.md` in the same PR. For a deeper catalog of what each harness -checks and what it needs to run, see [TESTING.md](TESTING.md). +Each feature also has its own headless gate; see +[Feature regression gates](#feature-regression-gates) below. Docs and changelog +rules are under [Pull requests](#pull-requests). + ## Checking a built app locally Run `npm run build` followed by `npm run preview`. Preview serves the built diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index 0ae94b746..403972d5e 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -304,35 +304,14 @@ the retrieval date (2026-07-30), exact download URL, license evidence, and the deterministic transform (`scripts/build-sf-neighborhoods.mjs`: `nhood` → `name`, ~2 m Douglas-Peucker simplification, 6-decimal rounding). -### Pilot CCTV source packs (`config/`) +### Local CCTV source packs (`config/`) -`config/cctv_sources.shinjuku.json` is a **demo source pack** (3 cameras: -Shinjuku Crossing, Shinjuku Station West, Shibuya Scramble) used for -projection-pipeline testing. The feeds are Google-hosted sample MP4 videos -(`storage.googleapis.com/gtv-videos-bucket/sample/`), not live public cameras. -Camera poses (coordinates, heading, pitch, FOV, mount height) are fabricated -priors placed at recognizable Tokyo intersections. - -Use the pack by setting: - -```bash -CCTV_SOURCES_FILE=config/cctv_sources.shinjuku.json npm run dev -``` - -Or equivalently, via `./scripts/dev-cctv.sh`: - -```bash -CCTV_SOURCES_FILE=config/cctv_sources.shinjuku.json ./scripts/dev-cctv.sh -``` - -The pack exercises the same CCTV registration and frustum-projection pipeline -as live city packs without requiring network access to a camera catalog. It is -**not** a live data source and carries no external attribution requirement. The -sample videos are publicly available Google test media. - -`config/cctv_sources.austin.json` is the reference live pack. Custom source -packs follow the same JSON schema (see either file); `CCTV_SOURCES_JSON` -accepts inline JSON as an alternative to a file path. +The server can read a local camera list from `CCTV_SOURCES_FILE` (default +`config/cctv_sources.austin.json`, which ships empty) and inline JSON from +`CCTV_SOURCES_JSON`; the two are merged, and inline entries win on a duplicate +id. A non-empty local pack replaces the live open-data camera packs unless +`CCTV_FORCE_AUSTIN=1` is set. The packs under `config/` show the schema, and a +pack's cameras keep their own source's license and attribution. --- diff --git a/changelog.d/210-contributor-docs-refresh.md b/changelog.d/210-contributor-docs-refresh.md new file mode 100644 index 000000000..ed6dbe354 --- /dev/null +++ b/changelog.d/210-contributor-docs-refresh.md @@ -0,0 +1,4 @@ +- Contributor docs: CONTRIBUTING lists what each of the three baseline checks + needs to run, DATA_SOURCES documents loading a local CCTV source pack, and + KNOWN-ISSUES points the floor-placement note at its verification script + instead of a missing report (Gaurav Patidar, @gaurav7902, #210). From d646fdbe60c40e7194a387067ed494100efb5e97 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Mon, 2 Nov 2026 07:39:02 -0800 Subject: [PATCH 12/12] Pin Node 24.22.2, the current 24 LTS patch, keeping 24.22.1 as the security floor --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/workflows/ci.yml | 8 ++++---- CONTRIBUTING.md | 4 ++-- Dockerfile | 2 +- changelog.d/1458-node-24-22-1-security-release.md | 7 ++++--- docs/CURRENT-STATE.md | 7 ++++--- 6 files changed, 16 insertions(+), 14 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 9069c648d..b753f8fef 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -40,7 +40,7 @@ body: attributes: label: OS, Node version, browser description: Include the versions you can find. Write "unknown" for anything unavailable. - placeholder: "Windows 11, Node 24.22.1, Chrome 151" + placeholder: "Windows 11, Node 24.22.2, Chrome 151" validations: required: true - type: input diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 766f0cc51..63d1eb29b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,10 +20,10 @@ jobs: strategy: fail-fast: false matrix: - node: [24.22.1, 26.x] + node: [24.22.2, 26.x] env: PUPPETEER_SKIP_DOWNLOAD: '1' - GEV_REQUIRE_ALLOCATION_GATE: ${{ matrix.node == '24.22.1' && '1' || '0' }} + GEV_REQUIRE_ALLOCATION_GATE: ${{ matrix.node == '24.22.2' && '1' || '0' }} steps: - name: Check out source uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 @@ -76,7 +76,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - node-version: 24.22.1 + node-version: 24.22.2 cache: npm - name: Install locked dependencies and headless Chrome @@ -131,7 +131,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - node-version: 24.22.1 + node-version: 24.22.2 cache: npm - name: Run the Pinokio install path diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5850fe650..9361e1f3c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,8 +11,8 @@ fixes, including CVE-2026-111497 (a Brotli decoder flaw). ```bash git clone https://github.com/bilawalsidhu/gods-eye-view.git cd gods-eye-view -nvm install 24.22.1 -nvm use 24.22.1 +nvm install 24.22.2 +nvm use 24.22.2 npm install npm run doctor ./scripts/dev-fresh.sh # or: npm run dev (keys are optional) diff --git a/Dockerfile b/Dockerfile index e711ec1d9..a9b8aebd9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ # proxies (ADS-B, AIS, Overpass, OpenAI Realtime, ...) as middleware, so there # is nothing else to containerise. See compose.yaml for the usual entry point. # Same Node as CI (package.json engines, .github/workflows), pinned by digest. -FROM node:24.22.1-slim@sha256:be8204d4bd327363357f532187b752ff62759c31bedf98e9e18fc08c9cbd605c +FROM node:24.22.2-slim@sha256:c86ecead9e55a5210ac1727de38a4ca840bf03dcaf34dc9749addbf383b1920c # Puppeteer is only used by the QA scripts; skip its ~150MB Chromium download. ENV PUPPETEER_SKIP_DOWNLOAD=1 diff --git a/changelog.d/1458-node-24-22-1-security-release.md b/changelog.d/1458-node-24-22-1-security-release.md index 8c8cdd318..d379a26f1 100644 --- a/changelog.d/1458-node-24-22-1-security-release.md +++ b/changelog.d/1458-node-24-22-1-security-release.md @@ -1,3 +1,4 @@ -- The Docker image and CI now pin Node 24.22.1, the first 24.x release with - the October 21, 2026 security fixes, including CVE-2026-111497 in the Brotli - decoder (Luis Ortiz, @anobjectn, #1458). +- The Docker image and CI now pin Node 24.22.2, the current 24 LTS patch. It + carries the October 21, 2026 security fixes first released in 24.22.1, + including CVE-2026-111497 in the Brotli decoder (Luis Ortiz, @anobjectn, + #1458). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 11df591b0..1792c73de 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -3436,7 +3436,7 @@ down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with > operator-side; the final accounting records only comparable population > reductions and deterministic Node allocation/solve measurements. Those > allocation gates were calibrated on Node.js 24.14.x and run in CI on the -> pinned Node.js 24.22.1 with the same ceilings; `package.json` permits +> pinned Node.js 24.22.2 with the same ceilings; `package.json` permits > supported product runtimes on Node 24 or 26, while the allocation runner > separately enforces Node 24 for these two calibrated probes. Each probe > compiles synchronously and discards explicit-GC @@ -6183,8 +6183,9 @@ The lockfile uses DOMPurify 3.4.17, protobufjs 8.8.0, PostCSS 8.5.28, source-map-js 1.2.2 and nanoid 3.3.19. Cesium remains on 1.138.0. Browser QA uses Puppeteer 25.10.0; image-processing tools use Sharp 0.35.5. QA scripts await Puppeteer's asynchronous executable-path lookup before testing or passing the path to Chrome. Supported Node versions remain -Node 24 (24.14.0 or later) and 26.x; the Docker image and CI pin 24.22.1, the first 24.x release with -the October 21, 2026 Node.js security fixes (including CVE-2026-111497). Use +Node 24 (24.14.0 or later) and 26.x; the Docker image and CI pin 24.22.2, +the current 24 LTS patch, which carries the October 21, 2026 Node.js security +fixes first released in 24.22.1 (including CVE-2026-111497). Use `npm ci` to reproduce the checked-in dependency tree. ### Traffic city navigation