From d04939b813da2e9d5f4a0bc3e056616d78a57c90 Mon Sep 17 00:00:00 2001 From: Gaurav Patidar Date: Wed, 9 Sep 2026 13:23:59 +0530 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 e9c6f2541f10ebc06ffbb9a9885bfc856224c835 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Mon, 2 Nov 2026 07:10:40 -0800 Subject: [PATCH 6/8] 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 7/8] 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 8/8] 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).