diff --git a/.env.example b/.env.example index 6f4544e00..814ad948d 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: @@ -110,7 +122,7 @@ PORT=4173 # Optional: Live AIS vessels layer # Browser fetch path. Keep same-origin so the private stream key stays server-side. -VITE_AIS_LIVE_API_URL=/api/ais-live +VITE_AIS_LIVE_API_URL=/api/vessels # Client render cap. The server can keep a larger AISStream cache, but drawing # every global vessel at once can overwhelm WebGL on laptops. VITE_AIS_LIVE_MAX_ROWS=12000 @@ -138,7 +150,7 @@ AISSTREAM_API_KEY= # Optional: TomTom live traffic flow (BYOK freemium — the free tier is 200,000 # Traffic Flow & Incidents tile requests per MONTH, not per day; no credit card -# required. Get a key at https://developer.tomtom.com). SERVER-SIDE ONLY: +# required. Get a key at https://my.tomtom.com/keys). SERVER-SIDE ONLY: # the browser fetches same-origin /api/tomtom/* and never sees the key. # Keyless fallback: the traffic layer runs its built-in simulation (white dots, # hardcoded per-road-class speeds) — no key required for the layer to work. @@ -182,9 +194,18 @@ AISSTREAM_API_KEY= # CCTV_CALGARY_ROWS_URL= # override the Open Calgary catalog URL # CCTV_CATALONIA_ENABLED=1 # set 0 to disable the Servei Català de Trànsit cameras # CCTV_CATALONIA_MAX_SOURCES=150 # cap cameras loaded from Catalonia (~135 exist today) +# CCTV_VEGVESEN_ENABLED=1 # set 0 to disable the Statens vegvesen (Norway) road cameras +# CCTV_VEGVESEN_MAX_SOURCES=900 # cap cameras loaded from Statens vegvesen (default loads all ~850 live; max 1000) +# CCTV_VEGVESEN_VIDEO=1 # set 0 to show stills instead of live HLS for Statens vegvesen video cameras +# CCTV_VEGVESEN_URL= # override the Statens vegvesen catalog URL # CCTV_MAX_SOURCES=4000 # catalog-wide ceiling, shared round-robin across packs # CCTV_PREFER_AUSTIN=1 # prioritize Austin cameras in the registry # CCTV_FORCE_AUSTIN= # force live default sources when a source-pack JSON is configured # 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 244c2a386..2b92201f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,118 @@ # Changelog +## [Unreleased] + +- Add Norway's road cameras from Statens vegvesen as a CCTV pack. The camera + list is one keyless GeoJSON request to the agency's OGC API view of its + DATEX 3.1 CCTV table, so no DATEX account is needed. Every working camera + loads (~850; faulty ones are dropped) under one "Norway" category, and the + ~135 working cameras that publish HLS play as live video through the existing in-memory + relay with the still as fallback. Frames and manifests are pinned to each + camera's own path on the agency hosts. `CCTV_VEGVESEN_ENABLED=0` disables + the pack, `CCTV_VEGVESEN_MAX_SOURCES` caps it (keeping cameras nearest the + largest cities) and `CCTV_VEGVESEN_VIDEO=0` keeps stills only. Attributed + under NLOD 2.0. + +## [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 @@ -182,7 +295,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 @@ -211,7 +324,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, @@ -236,7 +349,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 @@ -511,26 +624,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 @@ -550,32 +663,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. @@ -583,20 +696,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. @@ -607,7 +720,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. @@ -628,7 +741,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 @@ -638,7 +751,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 @@ -705,7 +818,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. @@ -833,9 +946,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 @@ -866,7 +979,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 @@ -945,7 +1058,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 @@ -973,7 +1086,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 @@ -988,7 +1101,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 @@ -1022,7 +1135,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 @@ -1113,7 +1226,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 @@ -1155,7 +1268,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 @@ -1175,6 +1288,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 @@ -1412,9 +1532,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/DATA_SOURCES.md b/DATA_SOURCES.md index c92384cbc..2d3a1538e 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -36,7 +36,7 @@ How to read this: | **InciWeb** (inciweb.wildfire.gov) | Per-incident "InciWeb ↗" card links to official incident pages (narratives, evacuation/closure notices) | U.S. government public incident information. Links are matched best-effort by incident name and state, with incident page origin and update times checked on selection. Same-origin `/api/fire-perimeters/inciweb` routes cache the catalog for 1 hour and publication metadata for 30 minutes; linked pages remain InciWeb content | "Incident information: InciWeb (inciweb.wildfire.gov)" | | **OpenFreeMap / OpenMapTiles** | Vector tiles | OpenStreetMap data under ODbL 1.0 | OpenFreeMap © OpenMapTiles | | **Overture Maps Foundation** | Military area names pack | [ODbL 1.0](https://opendatacommons.org/licenses/odbl/1-0/) | Overture Maps Foundation | -| **TomTom Traffic API** (flow vector tiles) | Traffic roads and live congestion (optional, BYOK) | [TomTom for Developers terms](https://developer.tomtom.com) (proprietary, your own key; free tier currently 200K tile requests/month — see [current pricing](https://docs.tomtom.com/pricing/)) | "Traffic flow data © TomTom" — registered when live mode activates | +| **TomTom Traffic API** (flow vector tiles) | Traffic roads and live congestion (optional, BYOK) | [TomTom for Developers terms](https://docs.tomtom.com/legal/terms-and-conditions) (proprietary, your own key; free tier currently 200K tile requests/month — see [current pricing](https://docs.tomtom.com/pricing/)) | "Traffic flow data © TomTom" — registered when live mode activates | | **Photon** (komoot) | Keyless place search — the fallback when no Google Maps key is configured, or when Google declines the Geocoding request | Service: free public instance, [fair use](https://photon.komoot.io) (no bulk/heavy use); underlying data: **ODbL 1.0** | "Photon (komoot)" | | **Open-Meteo** | Current weather in the cockpit Local Info page and cockpit-local dynamic atmospheric effects | [CC BY 4.0 data licence and adjacent-link attribution requirement](https://open-meteo.com/en/licence) | Linked "Weather data by Open-Meteo.com" beside the displayed local data | | **Google News RSS** | Primary locality-matched headlines in the cockpit Regional News page | [Google News Terms of Service](https://www.google.com/intl/en_us/terms_google_news.html) restrict use to personal, noncommercial use; linked articles remain third-party publisher content and retain publisher terms | "Google News RSS" plus each article's linked publisher/domain | @@ -55,6 +55,7 @@ How to read this: | **DriveBC** (Government of British Columbia) | CCTV camera catalog + frames, British Columbia highways | [Open Government Licence – British Columbia](https://www2.gov.bc.ca/gov/content/data/open-data/open-government-licence-bc) — attribution REQUIRED | "DriveBC. Contains information licensed under the Open Government Licence – British Columbia" | | **Open Calgary** (`data.calgary.ca`) | CCTV camera catalog + frames, Calgary | [Open Government Licence – City of Calgary](https://data.calgary.ca/stories/s/Open-Calgary-Terms-of-Use/u45n-7awa) — attribution REQUIRED | "Contains information licensed under the Open Government Licence – City of Calgary" | | **Servei Català de Trànsit** (+ Ajuntament de Barcelona, Ajuntament de Terrassa) | CCTV camera catalog + frames, Catalonia | [Llicència oberta d'ús d'informació - Catalunya](https://web.gencat.cat/ca/generalitat/dades-indicadors/dades-obertes/llicencies) — attribution REQUIRED | "Servei Català de Trànsit"; partner-supplied cameras also name their owner | +| **Statens vegvesen** (catalog `ogckart-sn1.atlas.vegvesen.no`, frames `kamera.atlas.vegvesen.no`, HLS `kamera.vegvesen.no`) | CCTV camera catalog + frames + live HLS video, Norway road cameras | [NLOD 2.0](https://data.norge.no/nlod/en/2.0) — attribution REQUIRED | "Contains data under the Norwegian licence for Open Government data (NLOD) distributed by Statens vegvesen" | | **GBFS (Lyft / BCycle)** | Bikeshare availability | Per-feed (attribution-only) | Credit the operator (e.g. Austin BCycle) + its `license_url` | | **MBTA / MassDOT** (cdn.mbta.com) | Transit layer live vehicles and up to 15 minutes of recently observed positions, Greater Boston | MBTA / MassDOT open realtime vehicle data, used for the live view and up to fifteen minutes of recently observed positions; published for developer use and not against their use. | Attribution to MBTA / MassDOT (courtesy text) | | **CapMetro** (data.texas.gov) | Transit layer live vehicles, Austin TX | CapMetro open realtime vehicle data, used for the live view; published for developer use and not against their use. | Attribution to CapMetro (courtesy text) | @@ -90,6 +91,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei - **Live Traffic NSW cameras.** The camera list is the keyless public `data.livetraffic.com/cameras/traffic-cam.json` feed (Transport for NSW open data, CC BY 4.0); frames are stills on `webcams.transport.nsw.gov.au`, and only that host is registered. That image host answers non-browser clients with an HTML placeholder, so the proxy identifies as a browser for that one host and as itself everywhere else. Frames are fetched at request time and never stored or redistributed. Disable with `CCTV_NSW_ENABLED=0`. - **Open Calgary traffic cameras.** Keyless. The camera list is the public Socrata dataset `k7p9-kppz` on `data.calgary.ca`; frames are stills on the city's own `trafficcam.calgary.ca` host, and only that host is registered. Most rows publish an `http://` frame URL and the host redirects those to HTTPS, so URLs are upgraded and pinned to that origin before registration. Frames are fetched at request time and never stored or redistributed. The dataset publishes no camera facing: the `quadrant` field and the quadrant suffix on each camera name are Calgary's address grid, not a bearing, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. The licence requires the attribution statement above. Disable with `CCTV_CALGARY_ENABLED=0`. - **Catalonia (Servei Català de Trànsit) cameras.** The camera list is the keyless `https://www.gencat.cat/transit/opendata/cameres.xml` WFS/GML feed, fetched over https with redirects refused and the body read capped at 1 MiB, since the list decides ids, coordinates and which allowlisted frame path is proxied. It carries a `font` field naming the real publisher per camera; this pack keeps SCT's own highway cameras plus hotlinked Barcelona (IMI) and Terrassa municipal cameras (~135 total), each confirmed to return a real frame, so frame URLs are checked against a small host allowlist (`mct.gencat.cat`, `www.bcn.cat`, `emap.terrassa.cat`) rather than one pinned origin (a link carrying `user:pass@` userinfo is rejected even on an allowed host), and every non-SCT camera carries its real owner as a `credit` shown beside the shared `provider`. The feed carries no compass heading, so every camera loads at low heading confidence (same as TfL and Fintraffic). SCT's own feed link (`RenderService`) needed a workaround: it always 302s to a second endpoint (`TransitCamera`) on a Location the upstream hardcodes as plain http even when requested over https — refused by `fetchWithinHost`'s same-origin (scheme-included) redirect policy — and separately, `mct.gencat.cat`'s TLS setup fails the handshake with Node's fetch outright (`ERR_SSL_WRONG_SIGNATURE_TYPE`) even though other HTTP clients complete it. The loader therefore builds the `TransitCamera` URL itself, over plain http, from `RenderService`'s `sctidcam` id, rather than registering the (broken) upstream link — the only transport this one host actually serves to this runtime. Frames are fetched at request time and never stored or redistributed. Disable with `CCTV_CATALONIA_ENABLED=0`. +- **Statens vegvesen road cameras.** Keyless. The camera list is one GeoJSON request to the OGC API Features view of the official DATEX 3.1 CCTV site table (`ogckart-sn1.atlas.vegvesen.no/ogc/features/v1/collections/datex_3_1:CctvSimple/items`), so no DATEX account is needed; frames are stills on `kamera.atlas.vegvesen.no/api/images/`, and only that exact path per camera is registered. Cameras the feed reports as faulty are dropped. About 135 working cameras also publish live HLS (`kamera.vegvesen.no/public//manifest.m3u8`, MPEG-TS); those play as live video through the bounded in-memory HLS relay, with the still as the snapshot fallback, and only that exact manifest path per camera is registered. `CCTV_VEGVESEN_VIDEO=0` keeps every camera on stills. `orientationDescription` names the place a camera faces, not a bearing, so it goes into the label and headings use the id-hash fallback at low confidence. By default every live camera loads (~850); `CCTV_VEGVESEN_MAX_SOURCES` (8–1000) lowers the cap, keeping the cameras nearest Oslo, Bergen, Stavanger, Trondheim, Kristiansand, Bodø and Tromsø and `CCTV_VEGVESEN_ENABLED=0` turns the pack off. Frames are fetched at request time and never stored or redistributed. The NLOD attribution is registered in the Data attribution popover. - **Camera frame content.** Public camera frames can contain people, vehicles and license plates, and the camera layer does nothing with them beyond putting them on the map. A successful upstream response is relayed as the provider served it: nothing in the camera pipeline blurs, redacts, enhances or restores it, and nothing there runs face, plate or object detection or otherwise recognises or classifies what is in the picture. (The ALPR layer described below maps where plate-reader cameras are, from locations contributors tagged in OpenStreetMap; it reads nothing from any camera frame.) When an upstream has no frame for a camera, what you see instead is a Google Street View still or the app's own placeholder card; the panel's source badge names which of them you are looking at, and a refresh that fails after a frame has arrived leaves the last good picture on screen rather than replacing it. The browser resamples each frame for display: onto a fixed 1920x1080 surface for the camera's projection plane in the scene, and onto a 192x108 canvas for the small ambient card. That scaling is the only change made to the picture, and the only pixels the camera layer reads back are a 64x36 downscale hashed to tell whether the picture has changed. Live streams are piped through; the still path reads the image into memory to pass it on — TxDOT wraps its JPEG in a JSON envelope, which is decoded there — and no frame is written to disk (the disk cache, `.gev-cache/`, holds feed JSON for other layers and never camera imagery). Outside that pipeline, the voice assistant is the one feature that sends imagery anywhere: when it answers a question about what is in view it may attach a downscaled screenshot of the rendered scene — including any camera plane on screen — to the request it sends to the voice model. Frames are fetched only from server-registered camera URLs — a client cannot hand the proxy a URL (see [SECURITY.md](SECURITY.md)). What the app does stays inside the line README draws: no named-person search, no face recognition, no tracking individuals. - **Transit (GTFS-Realtime).** The proxy requests only registered operator feeds, validates redirects, bounds responses, and uses conditional requests, short snapshot caching and failure backoff. All seven registered regions are live-enabled; each can be disabled independently. Vehicles use delayed playback between reports, with report age and waiting states shown separately. Session-local selected trails are available for every feed. MBTA additionally retains up to 15 minutes of recently observed positions in the running proxy, so available history can survive a browser reload; server restart clears it. History reads never fetch upstream, and other feeds have no proxy retention. Nothing is archived to disk or collected in the background. Operator text credits are included as a courtesy and do not imply endorsement. Heights are aligned to work with Google 3D tiles; subway markers are shown at street level because these feeds do not supply depth. - **Radio Browser.** `/api/radio/stations` discovers official API mirrors, makes bounded and coalesced healthy/geolocated HTTPS-station queries, caches the normalized public-domain directory for 45 minutes, and may serve the last good catalog for up to seven days during an outage. Refreshes must meet minimum accepted-query and station coverage before replacing a warm catalog; schema-valid responses whose rows all fail the product's health policy do not count as successful queries. A usable partial cold catalog is explicitly `DEGRADED`, and malformed or empty successful payloads are rejected atomically. Every directory and click-count request rejects redirects, validates all resolved addresses as globally routable (including reserved/documentation IPv4 and special/non-global IPv6 exclusions), and pins the TLS connection to a validated address. Only MP3/AAC non-HLS directory rows with public HTTPS stream targets are returned; favicons are intentionally omitted. Pressing play connects one browser audio element directly to the selected broadcaster and calls the directory's click counter through known-ID-only `POST /api/radio/click/:uuid`. GEV never proxies, caches, records, bundles, or redistributes audio. Radio Browser supplies station-level tags, not dependable current-song or upcoming-program metadata, so Radio filtering never claims either. Direct playback exposes the listener's IP address to the broadcaster, whose own stream terms apply. diff --git a/README.md b/README.md index f6a3ae886..724ea18cb 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.) @@ -292,7 +294,7 @@ Nineteen layers and map sources. **Seventeen have a keyless path.** Some offer a | 🛰️ **Satellites** | 838-object catalog, color-coded by class with a live legend — the **DENSE** chip drops in the whole Starlink shell | CelesTrak | 🟢 | | 🌍 **Earthquakes** | Global seismic activity, last 24h | USGS | 🟢 | | 🚗 **Traffic** | Simulated vehicles on OSM roads. With TomTom, live flow speeds drive the simulation and congestion colors below ~8 km; individual vehicle positions are not live observations | TomTom + OSM | 🟢 simulation · 🟡 live flow speeds | -| 📹 **CCTV Mesh** | ~3,600 public cameras projected _into_ the 3D space — Austin · Texas (TxDOT) · California (Caltrans) · London (TfL) · Ontario (511) · Finland (Fintraffic) · British Columbia (DriveBC) · Estonia (Tallinn, Tarktee) · Delaware (DelDOT live video) · New South Wales (Live Traffic NSW) · Calgary. Positions are published; poses are estimated priors **you calibrate by dragging a gizmo on the camera itself** | City APIs | 🟢 | +| 📹 **CCTV Mesh** | ~3,900 public cameras projected _into_ the 3D space — Austin · Texas (TxDOT) · California (Caltrans) · London (TfL) · Ontario (511) · Finland (Fintraffic) · British Columbia (DriveBC) · Estonia (Tallinn, Tarktee) · Delaware (DelDOT live video) · New South Wales (Live Traffic NSW) · Calgary · Norway (Statens vegvesen). Positions are published; poses are estimated priors **you calibrate by dragging a gizmo on the camera itself** | City APIs | 🟢 | | 📷 **Mapped ALPR Cameras** | License-plate-reader camera locations tagged by OpenStreetMap contributors, loaded one city-sized view at a time, with **SHOW NEAREST**. Locations and tags only: no plate data, no video | OpenStreetMap (incl. DeFlock mapping) | 🟢 | | 📻 **Radio** | Geolocated world radio with an **analog tuner** — drag the needle across up to 750 stations and the globe flies to each broadcaster | Radio Browser / broadcasters | 🟢 | | 🚌 **Transit** | Live buses, trams, metros, trains and ferries with delayed playback between reports, selected-vehicle trails, and mode-coloured DETECT labels — Boston, Austin, Minneapolis, Helsinki, the Netherlands, Norway, South East Queensland | Operator GTFS-Realtime feeds | 🟢 | @@ -419,7 +421,7 @@ Six keys. Four have a free tier, and the two 🔴 ones are metered: | 🔴 | **OpenAI** | 🎙️ The voice experience + AI HUD summary. The mini model works; the standard model is noticeably smarter. Want Gemini or another provider behind the mic? PRs welcome | [platform.openai.com](https://platform.openai.com) — metered, see costs below | | 🟡 | **AISStream** | 🚢 Live global ships | [aisstream.io](https://aisstream.io) — free signup | | 🟡 | **NASA FIRMS** | 🔥 Live active fires | [firms.modaps.eosdis.nasa.gov](https://firms.modaps.eosdis.nasa.gov/api/map_key/) — free | -| 🟡 | **TomTom** | 🚦 Live flow speeds and congestion colors for the simulated traffic layer | [developer.tomtom.com](https://developer.tomtom.com) — free tier available | +| 🟡 | **TomTom** | 🚦 Live flow speeds and congestion colors for the simulated traffic layer | [my.tomtom.com](https://my.tomtom.com/keys) — free tier available | ![Diving from city-scale live congestion straight into an intersection's public camera](docs/media/05-traffic-to-cctv.gif) @@ -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..8a4256523 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,8 +18,8 @@ The golden rule: **secret-bearing API keys stay on the server side.** The dev/pr | Key | Where it lives | How the browser uses it | |-----|----------------|--------------------------| | `OPENAI_API_KEY` | Server only | Browser fetches a short-lived **ephemeral** Realtime session token from `/api/realtime/token`; the real key never ships | -| `AISSTREAM_API_KEY` | Server only | Server holds the AISStream websocket; browser polls the same-origin `/api/ais-live` cache | -| OpenSky OAuth (`OPENSKY_CLIENT_ID/SECRET`) | Server only | Server mints + refreshes the token behind `/api/opensky` | +| `AISSTREAM_API_KEY` | Server only | Server holds the AISStream websocket; browser polls the same-origin `/api/vessels` cache | +| OpenSky OAuth (`OPENSKY_CLIENT_ID/SECRET`) | Server only | Server mints + refreshes the token behind `/api/flights` | | `GOOGLE_MAPS_SERVER_API_KEY` (optional, #33) | Server only | Server calls Places (`/api/google/nearby-places`, `/api/google/text-search`) and the Street View fallback with this key; falls back to `GOOGLE_MAPS_API_KEY` when unset | ### Two deliberately client-side keys — restrict them @@ -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, /