diff --git a/.env.example b/.env.example index 67a1e74a7..68fb3481a 100644 --- a/.env.example +++ b/.env.example @@ -34,16 +34,19 @@ GOOGLE_MAPS_API_KEY= # single shared key keeps working — splitting is opt-in. # SERVER-SIDE ONLY. GOOGLE_MAPS_SERVER_API_KEY= -# Optional: opt-in per-IP rate limit for the Google Places cost endpoint -# (/api/google/nearby-places), in requests per minute. -# DEFAULT IS UNLIMITED — unset (or 0) means no throttling, unchanged behavior. -# Set a positive integer to cap per-IP Places quota spend when self-hosting. -# Recommended whenever HOST is not localhost. NOTE: this is a per-IP, -# process-local guard (in-memory, resets on restart; over-limit requests get a -# sanitized 429) — it is NOT a billing cap. For hard spend protection set -# provider-side budget alerts: Google Cloud Console -> Billing -> Budgets & alerts -# (plus per-API quotas under APIs & Services -> Quotas). -# GEV_RATELIMIT_GOOGLE_PER_MIN=60 +# Per-IP rate limit for the Google Places cost endpoints +# (/api/google/nearby-places + /api/google/text-search), in requests per minute. +# DEFAULTS TO 120, the value the Pinokio build already ships. Raise or lower it +# with a positive integer; 60 is a sensible tightening whenever HOST is not +# localhost. Set it to exactly 0 to run unthrottled. An unreadable value (a +# typo, a stray unit) falls back to the default rather than to unlimited, so a +# slip cannot quietly disarm the guard. A positive value below 1 counts as 1. +# NOTE: this is a per-IP, process-local guard (in-memory, resets on restart; +# over-limit requests get a sanitized 429) — it is NOT a billing cap. For hard +# spend protection set provider-side budget alerts: Google Cloud Console -> +# Billing -> Budgets & alerts (plus per-API quotas under APIs & Services -> +# Quotas). +# GEV_RATELIMIT_GOOGLE_PER_MIN=60 # uncomment to tighten from the default 120 # Optional: Google Photorealistic 3D Tiles through Cesium ion, Bing world # imagery, and Cesium World Terrain. The free Community plan is for eligible @@ -65,15 +68,19 @@ OPENAI_REALTIME_REASONING_EFFORT=low OPENAI_REALTIME_CONTEXT_TOKENS=3000 OPENAI_REALTIME_CONTEXT_RETENTION=0.5 OPENAI_HUD_SUMMARY_MODEL=gpt-5-nano -# Optional: opt-in per-IP rate limit for the OpenAI cost endpoints +# Per-IP rate limit for the OpenAI cost endpoints # (/api/realtime/token + /api/openai/hud-summary), in requests per minute. -# DEFAULT IS UNLIMITED — unset (or 0) means no throttling, unchanged behavior. -# Set a positive integer to cap per-IP spend when self-hosting on a network. -# Recommended whenever HOST is not localhost. NOTE: this is a per-IP, -# process-local guard (in-memory, resets on restart; over-limit requests get a -# sanitized 429) — it is NOT a billing cap. For hard spend protection set a -# provider-side budget: OpenAI platform -> Settings -> Limits (usage limits). -# GEV_RATELIMIT_OPENAI_PER_MIN=30 +# DEFAULTS TO 30, the value the Pinokio build also ships. That clears ordinary +# use several times over: an open tab asks for a HUD summary every 15s (4/min) +# and a voice session mints one token. Raise or lower it with a positive +# integer; set it to exactly 0 to run unthrottled. An unreadable value (a typo, +# a stray unit) falls back to the default rather than to unlimited, so a slip +# cannot quietly disarm the guard. A positive value below 1 counts as 1. +# NOTE: this is a per-IP, process-local guard (in-memory, resets on restart; +# over-limit requests get a sanitized 429) — it is NOT a billing cap. For hard +# spend protection set a provider-side budget: OpenAI platform -> Settings -> +# Limits (usage limits). +# GEV_RATELIMIT_OPENAI_PER_MIN=30 # the default; uncomment only to change it # Optional: OpenSky auth mode (oauth | basic | auto | anon) # Default: oauth — requires client credentials below @@ -101,6 +108,11 @@ PORT=4173 # networks you trust, and consider the GEV_RATELIMIT_* throttles above. # HOST=localhost # HOST=0.0.0.0 +# Optional: comma-separated LAN hostnames to accept, in addition to localhost +# and IP addresses, for example GEV_ALLOWED_HOSTS=globe.lan,globe.internal. +# Binding HOST=0.0.0.0 alone does NOT trust every Host header; suffix and +# wildcard entries are ignored. +# GEV_ALLOWED_HOSTS= # Optional: NASA FIRMS live active fires. SERVER-SIDE ONLY (never reaches the # browser — the client polls the /api/firms proxy). Free key: @@ -186,3 +198,8 @@ AISSTREAM_API_KEY= # Optional: comma-separated Overpass URLs; use only instances you run or pay for. OVERPASS_UPSTREAMS= + +# Optional: who may frame the app in embed mode (?embed=1), as CSP +# frame-ancestors sources such as "https://example.com", or * for any page. +# Unset, no page may frame it. Other pages of the app can never be framed. +# GEV_EMBED_FRAME_ANCESTORS= diff --git a/CHANGELOG.md b/CHANGELOG.md index d52bd0718..563ac4319 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,111 @@ # Changelog -- Bound the remaining upstream body reads (FIRMS, CelesTrak, adsb.lol, TomTom, OpenSky, adsbdb) with streaming size caps and shared deadlines; the adsb.lol military and OpenSky `states/all` polls gain deadlines of their own. +## [Unreleased] + +- Bound the remaining upstream body reads (FIRMS, CelesTrak, adsb.lol, + TomTom, OpenSky, adsbdb) with streaming size caps and shared deadlines; the + adsb.lol military poll gains a deadline of its own. + +## [0.2.1] — 2026-10-02 — God's Eye View through Your Agent (with security fixes and other features) + +- Ship each bundled data pack once. The region, marine, admin-boundary, + county, military-name and neighborhood packs were emitted twice by the + production build, as the JSON the browser fetches and as an unused + JavaScript copy; `dist/` drops from 54 MB to 42 MB. Under Node the loader + reads the JSON file directly (reporting that Node 24.14 or newer is needed + on a runtime too old to do so), and a test keeps app code from importing a + pack as a module. + +- Apply the Host check before the app's own routes. Vite installs its Host + check after the middleware that plugins add, so the provider and `/api` + routes used to answer any Host, including a DNS-rebinding name, in every + mode. A first-running middleware now applies the same allowed hosts on the + dev and preview servers. + +- Refuse cross-site browser requests to the cost-bearing and log endpoints + (`/api/realtime/token`, `/api/openai/hud-summary`, + `/api/google/nearby-places`, `/api/google/text-search`, + `/api/realtime/debug-log`): a foreign or opaque + Origin, a cross-site `Sec-Fetch-Site`, or proxy forwarding headers get 403, + while loopback tools and LAN use keep working. The dev and preview servers + send one shared Content-Security-Policy (James Sumpter, #242). + +- The voice debug log records the server's own time: a record can no longer + supply its `loggedAt`. The dev and preview servers also send + `X-Content-Type-Options: nosniff` (findings by Sunil, #710). + +- Keep the Host-header check when binding to all interfaces. `HOST=0.0.0.0` + used to accept any Host; the dev and preview servers now accept IP + addresses, `localhost` and the LAN hostnames listed in `GEV_ALLOWED_HOSTS` + (suffix and wildcard entries are ignored), which also keeps DNS-rebinding + names out in LAN mode. The `.local` suffix is no longer accepted by default; + list such a name explicitly (Puspo Aditya, #97, fixes #21). + +- Throttle the cost-bearing proxies by default instead of on request. The + OpenAI endpoints (`/api/realtime/token`, `/api/openai/hud-summary`) now allow + 30 requests per minute per client IP and the Google Places endpoints + (`/api/google/nearby-places`, `/api/google/text-search`) 120 — the caps the + Pinokio build already ships, so the packaged app is unaffected and only an + unconfigured server changes, from unlimited to what the product already runs + with. `GEV_RATELIMIT_OPENAI_PER_MIN` and `GEV_RATELIMIT_GOOGLE_PER_MIN` still + override the caps, and exactly `0` disables them; a value that cannot be read + as a number now falls back to the default rather than to unlimited, so a typo + cannot silently disarm the guard (daikaginza, #683). + +- Tighten the local MCP server and embed mode. The panel's `panel_request` + needs the key in its MCP server's panel page, and refuses Provider + Settings, credential and model endpoints, `/mcp` and development server + routes in any letter case, encoding or dot suffix; panels on one server share six + requests in flight, and further requests wait their turn. `/mcp` applies Provider Settings' proxy and sharing checks, + requires a Host on the port it reached and an Origin from that host, and + times out request bodies after 30 seconds; stdio honors + `notifications/cancelled`. Embed mode is no longer framable unless + `GEV_EMBED_FRAME_ANCESTORS` allows the framing page, and view answers go + only to the origin that sent the view. SECURITY.md describes the MCP surface + and the panel's browser keys. + +- Cockpit now enters on the existing matching map style while keeping one fixed, + duplicate-free carousel over `Normal / CRT / NVG / FLIR / Anime / Noir / Snow`. + Normal remains a real unfiltered option; Cockpit-only choices still restore + the captured map style through both Exit Cockpit and Reset. +- Cyber's compact right-rail and Cockpit utility buttons now center their glyphs + vertically and share the same inset and edge alignment. +- Keep Cyber Voice help/error popups and Location/Visual Presets pins clear of + their decorative frames. Leave space above attribution for its full logo row. +- Add an intentionally future-facing, opt-in panel surface contract for new + panels to inherit compatible Normal, Cyber and Cockpit styling. Its first + production adopter will land separately after this change. +- Restore the user's previous visual preset when they explicitly switch from + Cyber to another HUD layout, without overriding scene or shared-link state. +- Move the aligned Cyber side-panel rails upward on desktop so the left stack + clears the lower coordinate card; keep Cockpit's independent visor layout. + +## [0.2.0] — 2026-10-02 — God's Eye View through Your Agent + +- Show God's Eye View inside AI conversations. Answers that can be shown + include a view (camera, layers, style, map, marks, something to follow), + and `show_in_gods_eye_view` displays it as the live globe in clients that + support MCP Apps, such as Claude Desktop and the Codex and ChatGPT desktop + apps, and as a link everywhere else. The panel loads the app's panel build + (`npm run build:panel`) and its data through the MCP server, so a local + server needs no HTTPS or tunnel. `?embed=1` shows the globe alone for pages + that frame the app. + +- Add tools for language-model clients and a local MCP server, served over + stdio by `npm run mcp` and over HTTP at `/mcp` on the development and preview + servers for local requests only. `npm run mcp` + serves earthquake, active-fire, launch, aircraft, ship, satellite, camera, radio, + place, routing, bike-share, transit, traffic, weather, cyclone, fire-perimeter, + terrain, installation and map-feature queries, plus a situation brief, over + stdio to clients such as Claude Code, reading + from a running app. Tools are defined + once in `gods-eye-view/tools`, reuse the layers' source factories, and are + exposed through the protocol adapter in `gods-eye-view/tools/mcp`. The + catalog also covers weather maps, wind, recent imagery, submarine cables, + license plate reader cameras, datacenters and dams, the Bhote Koshi flood, + military awareness and links that open the app over an area with layers on. + Voice offers the same queries next to its app actions. + - Public Overpass instances are no longer used by default. Street Traffic roads come from TomTom flow tiles, OpenFreeMap vector tiles, or both, chosen on the layer row (TomTom / OSM / Hybrid) or with `?trafficRoads=`. With a @@ -183,7 +288,7 @@ (`src/ui/imagerySplit.js`, `src/maps/imageryComparison.js`), and `MapSourceController.subscribe()` reports every settled map switch. -## Unreleased — local receiver feeds +### Local receiver feeds - The Local ADS-B layer also reads local 1090 MHz and 978 MHz UAT decoder feeds: the `aircraft.json` that dump1090-fa, readsb, tar1090 or skyaware978 @@ -212,7 +317,7 @@ - Records carry `band` (`1090`/`978`) and `source` (`webusb`/`feed`). - See `docs/LOCAL-RECEIVERS.md`. -## Unreleased — local RTL-SDR and Local ADS-B +### Local RTL-SDR and Local ADS-B - Add a Local RTL-SDR card to the Radio panel. It connects a USB RTL-SDR in desktop Chrome or Edge through WebUSB and receives broadcast FM (tune, seek, @@ -237,7 +342,7 @@ - Add `@jtarrio/webrtlsdr` and `@jtarrio/signals` (Apache-2.0); see `THIRD_PARTY_NOTICES.md`. -## Unreleased — weather review +### Weather review - On 3D Tiles, draw a 4096×2048 detail window around the view on each observed-weather shell except global infrared, sampled by the shell's own @@ -503,26 +608,26 @@ Analyst records for loaded satellites, datacenters and dams, with explicit bound - Split application scene, controls, catalog, tools and HTML into reusable components; configure application request services and sources without changing global fetch. Preserve standalone markup and voice behavior. Explicit annotation navigation may resolve a distant named target. -## Satellite pass prediction +### Satellite pass prediction - Bisect pass rise/set to ~0.2 s and fit peak elevation with a parabola. - Mark passes visible from Earth-shadow and civil-twilight checks. - Add `getNextSatellitePass(noradId, options)` for any loaded catalog satellite. - `next_iss_pass` retains the next geometric pass and adds visibility metadata. `next_satellite_pass` adds bounded loaded-catalog name/NORAD lookup and optional visible-only filtering (Rehaan Delmotra, #451; maintainer adaptation). -## Voice component boundaries +### Voice component boundaries - Separate voice controls, Realtime connection requests and the action runner. - Allow compatible endpoints and server-selected models through construction options. - Cancel pending token/SDP requests on Stop or teardown and reject expired secrets. -## Configurable geospatial services +### Configurable geospatial services - Compose geocoding, place context and routes through independent providers. - Allow compatible endpoint configuration without changing voice tools or annotation behavior. - Isolate configured source caches and reject results after cancellation. -## ALPR camera locations +### ALPR camera locations - Port Manjunath's (@manjunath22466) cyan camera badges, coral selection brackets, gradient direction wedges and animated tactical labels into the reusable ALPR @@ -542,32 +647,32 @@ Analyst records for loaded satellites, datacenters and dams, with explicit bound - Separate the request adapter, camera model, presentation, and instance lifecycle. Source cancellation also guards late response bodies and rejects invalid query bounds. -## Release disabled infrastructure rendering +### Release disabled infrastructure rendering - Remove built Data Center, Dam and Submarine Cable entities when their layers are disabled, avoiding retained visualizer work and entity memory. - Keep parsed datasets cached for re-enable; rebuild entities without refetching. -## Camera layer components +### Camera layer components - Separate camera source requests, placement, frames, projection, cards and calibration. - Own visibility listeners and pending initialization within each layer lifetime. - Preserve existing camera catalogs, URL families, geometry and playback behavior. -## Traffic and bikeshare components +### Traffic and bikeshare components - Separate traffic loading, animation, styling and lifecycle into factory-owned components. - Give each flow source its own bounded decode cache and cancellation checks. - Separate bikeshare registry, station requests, rendering, selection and proximity handling. -## Installation and context components +### Installation and context components - Separate mapped-site requests, records, placement, selection and viewport lifecycle. - Separate proximity queries, subject tracking, navigation/history, panel and direction rendering. - Retain source and ground-floor ownership in standalone composition; reject malformed installation snapshots and ignore failures from cancelled requests. -## Satellite and mission layer components +### Satellite and mission layer components - Separate catalog loading, orbit calculations, display, tracking and interaction into instance-owned satellite components. @@ -575,20 +680,20 @@ Analyst records for loaded satellites, datacenters and dams, with explicit bound operations, retaining existing layer controls and satellite coordination. - Cancel late mission source work and reject malformed launch snapshots. -## Fire layer components +### Fire layer components - Split fire source loading, state, rendering, cards, selection and viewport work into reusable components with application-owned scene services. - Cancel late refreshes, retain good data after malformed responses, and preserve selection identity without repeating a user-selection notification on refresh. -## Earthquake components +### Earthquake components - Separate earthquake snapshot loading, record validation, and display ownership. - Cancel pending earthquake refreshes on disable or destruction, retaining the last good snapshot after malformed or failed refreshes. -## September 8, 2026 +### September 8, 2026 Earthquake refreshes validate the complete feed and construct replacement entities before clearing the previous snapshot. Malformed rows and duplicate rendered IDs retain the last good entities, overlays, count and timestamp and report a malformed response; unknown magnitude is excluded from M2.5+ rendering. @@ -599,7 +704,7 @@ Launch payloads with missing records now say PAYLOAD DATA UNAVAILABLE. Missing n This changelog records public product changes. For the authoritative description of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md). -## [Unreleased] +### Other changes - Add ECMWF IFS model selection to Wind (#464, thanks @beneduzi), with model-scoped forecast-step caches, cancellation of replaced requests, and separate issue/valid timestamps. @@ -620,7 +725,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md - Separate submarine cable sources and rendering components, and export bundled geography lookup modules. -### Added +#### Added - DISPLAY ▸ Draw: draw on the world by hand. Pick Area, Line or Pin, click the vertices, double-click or press Enter to finish, label and colour it; Backspace @@ -630,7 +735,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md together. While you are drawing, the draw tool owns the pointer and no layer selects what you click through (#235 — thanks @cora-fresh-labs). -### Fixed +#### Fixed - Keep traffic-road bounds crossing the antimeridian monotonic and inside the longitude range accepted by the Overpass request path, preserving the small @@ -697,7 +802,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md toggle. Camera departure cancels pending work, arrival checks the final view, and superseded requests cannot keep a newer view loading. -### Added +#### Added - Add MODIS NRT (Terra+Aqua, ~1 km) active fires to the FIRMS layer, sharing the existing `FIRMS_MAP_KEY` and 30-minute cache. @@ -825,9 +930,9 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md - Extract shared surface keyboard handling for the welcome launcher and Provider Settings, preserving Tab/Escape behavior and releasing the listener on teardown. -### Added +#### Added -### Security +#### Security - The CCTV media route no longer forwards a client `Range` header to the upstream camera host as it arrived. A single `bytes=` range is canonicalized and @@ -858,7 +963,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md - Cancel the active location lookup when its controls are disposed. -### Fixed +#### Fixed - `DATA_SOURCES.md` states what the project does with camera frame content: a successful upstream response is relayed as the provider served it, nothing in @@ -937,7 +1042,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md fetching, AIS records/tracks and shared request helpers; preserve existing routes, local setup, fallback behavior and rendering. -### Added +#### Added - **Directions layer** — keyless A→B directions without a geocoder or a microphone (thanks @spcpza). The row's chips arm a globe click for A and B @@ -965,7 +1070,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md voice routes since launch, previously uncredited), with the OpenStreetMap credit and the "fix the map" link the service's usage policy asks for. -### Changed +#### Changed - The interface asks Google Fonts for only the icon glyphs it draws, instead of the whole variable icon font, and no longer requests a second icon family that @@ -980,7 +1085,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md - Rename standalone browser startup to `src/standalone/` and add a Node-only `gods-eye-view/build/vite` export with checked package ownership. -### Development +#### Development - The CCTV launcher and preview-server tests resolve their temporary fixture root through `fs.realpath`, so they pass on macOS, where the system temp @@ -1014,7 +1119,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md infrastructure modules and their consumer tests. Package boundary checks keep those exports separate from app startup and local Node services. -### Fixed +#### Fixed - Reduce terrain-height timeouts when Re:Earth slows down. Batches are sized against measured response latency on both browser and server to reduce @@ -1105,7 +1210,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md and upstream error bodies; response statuses and cache fallback remain intact. Includes the security fixes contributed by Tom-Neverwinter in PR #171. -### Fixed +#### Fixed - Map Source keyboard opening retries focus until the selected tile is visible. Leaving the disclosure, pointer interaction, or closing the tray cancels the @@ -1147,7 +1252,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md that had since recovered. A miss is now cached only when every source consulted actually returned a verdict. -### Added +#### Added - Keyless place search. The LOCATION search box and the `fly_to_location` voice tool now resolve place names through Photon (komoot, over OpenStreetMap) when @@ -1167,6 +1272,13 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md to Puppeteer 25.10.0 and Sharp 0.35.4. Cesium remains on 1.138.0. Browser QA awaits the new asynchronous executable-path lookup. +### Live CCTV video + +- Live HLS video shares one decoder between the camera panel and projection, + with a DelDOT HTTPS source pack. Credit: Daniel Slay (@Danielslay86), PR #489. +- Maintainer adjustments bound sessions and downloads, remove disk/subprocess + remuxing, reject redirects, and clean up playback on switching or disabling. + ## [0.1.1] — 2026-09-01 — Installation and live-data fixes ### Changed @@ -1404,9 +1516,3 @@ represent previously published GitHub Releases. - Initial project version. -### Live CCTV integration candidate - -- Live HLS video shares one decoder between the camera panel and projection, - with a DelDOT HTTPS source pack. Credit: Daniel Slay (@Danielslay86), PR #489. -- Maintainer adjustments bound sessions and downloads, remove disk/subprocess - remuxing, reject redirects, and clean up playback on switching or disabling. diff --git a/README.md b/README.md index f6a3ae886..85e66938d 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,8 @@ _Ask for radio near anywhere and the globe starts broadcasting — every station --- +**💬 Or ask from Claude and Codex.** God's Eye View is also an MCP server: ask Claude Desktop, Codex or ChatGPT desktop to show a place, and the live globe opens right in the conversation. Setup takes a minute — see [docs/MCP_SETUP.md](docs/MCP_SETUP.md). + ## 🛰️ What's on the Globe Nineteen layers and map sources. **Seventeen have a keyless path.** Some offer additional capabilities with a provider key. (🟢 no key · 🟡 free key · 🔴 metered.) @@ -510,7 +512,7 @@ Everything above is the deliberately cheap baseline — enough to get a real tas ### 🔒 Sharing an instance -By default nobody else can reach your server — it binds to localhost. To share on your LAN, opt in explicitly (`npm run dev -- --host 0.0.0.0 --port 4173`, or `HOST=0.0.0.0 ./scripts/dev-fresh.sh` on macOS/Linux) — but know that ⚠️ **a LAN-visible server brokers your configured API keys to anyone who can reach it.** Set the per-IP throttles (`GEV_RATELIMIT_OPENAI_PER_MIN`, `GEV_RATELIMIT_GOOGLE_PER_MIN` — see `.env.example`) and, before anything else, **configure provider quotas, usage limits, and billing alerts**: app-level throttles are not billing caps, and a budget alert alone does not stop spending. Full threat model in [SECURITY.md](SECURITY.md). +By default nobody else can reach your server — it binds to localhost. To share on your LAN, opt in explicitly (`npm run dev -- --host 0.0.0.0 --port 4173`, or `HOST=0.0.0.0 ./scripts/dev-fresh.sh` on macOS/Linux). IP addresses work as they are; if you use a LAN hostname, add it with `GEV_ALLOWED_HOSTS=globe.lan`, since binding to `0.0.0.0` does not trust arbitrary Host headers. But know that ⚠️ **a LAN-visible server brokers your configured API keys to anyone who can reach it.** Per-IP throttles on the cost-bearing endpoints are on by default (`GEV_RATELIMIT_OPENAI_PER_MIN`, `GEV_RATELIMIT_GOOGLE_PER_MIN` — see `.env.example` to tune them), but before anything else, **configure provider quotas, usage limits, and billing alerts**: app-level throttles are not billing caps, and a budget alert alone does not stop spending. Full threat model in [SECURITY.md](SECURITY.md). Provider Settings is disabled when the server is shared, so remote users cannot access the key-entry panel. diff --git a/SECURITY.md b/SECURITY.md index 718c43778..71e4ce922 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -71,14 +71,16 @@ The data proxies under `server/providers/` are written so the browser cannot tur - **Sanitized errors** — internal error details are not echoed back to clients. - **Coalesced OAuth refresh** and cached successful responses only (OpenSky). - **Redacted debug logging.** The voice debug log (`.gev-logs/`, gitignored) strips API keys, bearer tokens, client secrets, and image data URLs before writing. +- **Cross-site gate on cost/log endpoints.** The cost-bearing (`/api/realtime/token`, `/api/openai/hud-summary`, `/api/google/nearby-places`, `/api/google/text-search`) and log (`/api/realtime/debug-log`) endpoints refuse cross-site browser requests: a foreign or opaque `Origin`, a `Sec-Fetch-Site` other than `same-origin`/`none` (this is what blocks an ``/navigation that carries no `Origin`), or any reverse-proxy/CDN forwarding header returns `403`. Non-browser loopback tools (the repo QA harnesses POST the token endpoint with no `Origin`) and the explicit `HOST=0.0.0.0` LAN opt-in keep working — the gate deliberately does not require a loopback remote address; that stricter requirement belongs only to the credential panel (`admitKeySetupRequest`). The Host check (`allowedHosts`, applied by `hostCheckPlugin` ahead of every route, since Vite's own check runs only after plugin middleware) rejects a rebinding `Host` before any handler runs, in every mode, and the gate is defense in depth behind it: the LAN opt-in keeps that check and accepts only IP addresses, `localhost` and names listed in `GEV_ALLOWED_HOSTS`. **Limit:** a name you list there is trusted like the app's own address, so list only names you control. +- **Full Content-Security-Policy.** The dev and preview servers send a real CSP (`script-src 'self' 'unsafe-eval' blob:` plus the YouTube, Facebook and X scripts the Nepal event's embedded media loads, with a matching `frame-src` for their players and posts: no inline script and no other script origin; `blob:` covers the object URLs Cesium's bundled workers bootstrap through in the built app), alongside the existing `X-Frame-Options: DENY` and `frame-ancestors 'none'`. `'unsafe-eval'` stays because Knockout, bundled inside Cesium's widgets package, resolves the global object with `eval` at load time and the globe does not initialize without it; styles permit `'unsafe-inline'` and Google Fonts because Vite's dev client injects ` + + +
Waiting for a view…
+
+ + +
+ + + +`; +} + +/** + * The panel as an MCP resource. `runtime` is the panel's script, a function + * of its configuration that runs in the panel page; the application supplies + * it (src/app/globePanelRuntime.js), as it is browser code. The security + * policy lets the panel load map imagery, tiles and fonts from their + * providers; everything from the app's own server arrives through the MCP + * server instead. `panelKey` is the key the panel's requests must carry + * (see src/tools/queries/panelRequest.js), which this page holds. + */ +export function createGlobePanelResource({ runtime, panelKey }) { + if (typeof runtime !== 'function') + throw new TypeError('The globe panel needs its runtime script'); + if (typeof panelKey !== 'string' || panelKey.length === 0) + throw new TypeError('The globe panel needs its request key'); + return Object.freeze({ + uri: GLOBE_PANEL_URI, + name: 'globe', + title: "God's Eye View globe", + description: "Live God's Eye View, showing the view a tool returns.", + mimeType: MCP_APP_MIME_TYPE, + text: panelHtml(runtime, panelKey), + _meta: { + ui: { + csp: { + resourceDomains: [...PROVIDER_ORIGINS], + connectDomains: [...PROVIDER_ORIGINS], + }, + prefersBorder: true, + }, + }, + }); +} diff --git a/src/tools/globePanel.test.mjs b/src/tools/globePanel.test.mjs new file mode 100644 index 000000000..964ca0a64 --- /dev/null +++ b/src/tools/globePanel.test.mjs @@ -0,0 +1,135 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + GLOBE_PANEL_URI, + MCP_APP_MIME_TYPE, + createGlobePanelResource, +} from './globePanel.js'; +import { panelRuntime } from '../app/globePanelRuntime.js'; +import { composeCatalog, coreTools } from './index.js'; + +test('the globe panel is an MCP Apps resource that loads the app through its server', () => { + const resource = createGlobePanelResource({ + runtime: panelRuntime, + panelKey: 'test-key', + }); + assert.equal(resource.uri, GLOBE_PANEL_URI); + assert.equal(resource.mimeType, MCP_APP_MIME_TYPE); + assert.equal(MCP_APP_MIME_TYPE, 'text/html;profile=mcp-app'); + const { csp } = resource._meta.ui; + // The panel reaches only map and font providers; the app's own files + // and data come through the MCP server, so no app address is declared. + assert.deepEqual(csp.connectDomains, csp.resourceDomains); + assert.ok(csp.resourceDomains.includes('https://tile.googleapis.com')); + assert.ok( + csp.resourceDomains.every((origin) => origin.startsWith('https://')), + ); + assert.equal(csp.frameDomains, undefined); + assert.equal(csp.baseUriDomains, undefined); + assert.match(resource.text, /"toolName":"panel_request"/); + // The page carries the key its requests need, and sends it with each one. + assert.match(resource.text, /"panelKey":"test-key"/); + assert.match(resource.text, /key: config\.panelKey/); + assert.match(resource.text, /"panelBase":"\/panel\/"/); + assert.match(resource.text, /'ui\/initialize'/); + assert.match(resource.text, /'ui\/notifications\/tool-result'/); + assert.match(resource.text, /"protocolVersion":"2026-01-26"/); + // The MCP Apps SDK's initialize parameters; hosts reject anything else. + assert.match(resource.text, /appInfo: \{/); + assert.doesNotMatch(resource.text, /clientInfo/); + // Fullscreen where the host offers it. + assert.match(resource.text, /'ui\/request-display-mode'/); + assert.match(resource.text, /