style: format extracted local services

This commit is contained in:
Sameh Khamis committed 2026-09-12 13:01:49 -07:00
1 parent 233285fe12
commit 5223b6df5b
33 files changed
+1423 -468

No files matched your search

+32 -1
View File
@@ -83,5 +83,36 @@
"server/providers/radio/transport.js",
"src/tooling/mediaProviders.test.mjs",
"server/standalone/api-not-found.js",
"src/tooling/previewServing.test.mjs"
"src/tooling/previewServing.test.mjs",
"server/providers/common/request.js",
"server/providers/common/source-root.js",
"server/providers/local.js",
"server/providers/military-installations.js",
"server/providers/military-installations/cache.js",
"server/providers/military-installations/constants.js",
"server/providers/military-installations/query.js",
"server/providers/openai.js",
"server/providers/openai/constants.js",
"server/providers/openai/debug-log.js",
"server/providers/openai/hud-summary.js",
"server/providers/openai/instructions.js",
"server/providers/openai/rate-limit.js",
"server/providers/openai/realtime.js",
"server/providers/openai/tools.js",
"server/providers/overpass.js",
"server/providers/overpass/cache.js",
"server/providers/overpass/constants.js",
"server/providers/overpass/geometry.js",
"server/providers/overpass/query.js",
"server/providers/overpass/transport.js",
"server/providers/regional.js",
"server/providers/regional/briefing.js",
"server/providers/regional/http.js",
"server/providers/regional/news.js",
"server/providers/regional/place.js",
"server/providers/regional/query.js",
"server/providers/regional/weather-effects.js",
"server/providers/regional/weather.js",
"server/standalone/key-setup.js",
"src/tooling/localServices.test.mjs"
]
+39 -120
View File
@@ -10,47 +10,27 @@
"src/data/localGeojsonCore.js",
"src/data/localGeojsonLod.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
},
"application": {
"exports": [
"./application"
],
"modules": [
"src/app/application.js"
],
"exports": ["./application"],
"modules": ["src/app/application.js"],
"external": []
},
"viewer": {
"exports": [
"./application/viewer"
],
"modules": [
"src/app/viewer.js"
],
"external": [
"cesium"
]
"exports": ["./application/viewer"],
"modules": ["src/app/viewer.js"],
"external": ["cesium"]
},
"vite-build": {
"runtime": "node",
"exports": [
"./build/vite"
],
"modules": [
"build/vite.js"
],
"external": [
"vite-plugin-cesium"
]
"exports": ["./build/vite"],
"modules": ["build/vite.js"],
"external": ["vite-plugin-cesium"]
},
"live-providers": {
"runtime": "node",
"exports": [
"./server/providers/live"
],
"exports": ["./server/providers/live"],
"modules": [
"server/providers/live.js",
"server/providers/common/http.js",
@@ -65,24 +45,16 @@
"src/data/aisStreamAdapter.js",
"src/data/aisWatchdog.js"
],
"external": [
"ws"
]
"external": ["ws"]
},
"adsb-lol-source": {
"exports": [
"./sources/adsb-lol"
],
"modules": [
"src/data/adsbLolFallback.js"
],
"exports": ["./sources/adsb-lol"],
"modules": ["src/data/adsbLolFallback.js"],
"external": []
},
"place-providers": {
"runtime": "node",
"exports": [
"./server/providers/places"
],
"exports": ["./server/providers/places"],
"modules": [
"server/providers/places.js",
"server/providers/places/google.js",
@@ -97,19 +69,13 @@
"external": []
},
"place-payloads": {
"exports": [
"./sources/places"
],
"modules": [
"src/data/placeProviderPayloads.js"
],
"exports": ["./sources/places"],
"modules": ["src/data/placeProviderPayloads.js"],
"external": []
},
"space-providers": {
"runtime": "node",
"exports": [
"./server/providers/space"
],
"exports": ["./server/providers/space"],
"modules": [
"server/providers/space.js",
"server/providers/space/celestrak.js",
@@ -120,19 +86,13 @@
"external": []
},
"space-requests": {
"exports": [
"./sources/space"
],
"modules": [
"src/data/spaceProviderRequests.js"
],
"exports": ["./sources/space"],
"modules": ["src/data/spaceProviderRequests.js"],
"external": []
},
"terrain-provider": {
"runtime": "node",
"exports": [
"./server/providers/terrain"
],
"exports": ["./server/providers/terrain"],
"modules": [
"server/providers/terrain.js",
"src/data/terrainHeightsProxy.js"
@@ -140,70 +100,41 @@
"external": []
},
"terrain-source": {
"exports": [
"./sources/terrain"
],
"modules": [
"src/data/terrainHeightsProxy.js"
],
"exports": ["./sources/terrain"],
"modules": ["src/data/terrainHeightsProxy.js"],
"external": []
},
"traffic-provider": {
"runtime": "node",
"exports": [
"./server/providers/traffic"
],
"modules": [
"server/providers/traffic.js",
"src/data/tomtomTiles.js"
],
"exports": ["./server/providers/traffic"],
"modules": ["server/providers/traffic.js", "src/data/tomtomTiles.js"],
"external": []
},
"traffic-source": {
"exports": [
"./sources/traffic"
],
"modules": [
"src/data/tomtomTiles.js"
],
"exports": ["./sources/traffic"],
"modules": ["src/data/tomtomTiles.js"],
"external": []
},
"firms-provider": {
"runtime": "node",
"exports": [
"./server/providers/firms"
],
"modules": [
"server/providers/firms.js",
"src/data/firmsCsv.js"
],
"exports": ["./server/providers/firms"],
"modules": ["server/providers/firms.js", "src/data/firmsCsv.js"],
"external": []
},
"gbfs-provider": {
"runtime": "node",
"exports": [
"./server/providers/gbfs"
],
"modules": [
"server/providers/gbfs.js",
"src/data/gbfsSource.js"
],
"exports": ["./server/providers/gbfs"],
"modules": ["server/providers/gbfs.js", "src/data/gbfsSource.js"],
"external": []
},
"gbfs-source": {
"exports": [
"./sources/gbfs"
],
"modules": [
"src/data/gbfsSource.js"
],
"exports": ["./sources/gbfs"],
"modules": ["src/data/gbfsSource.js"],
"external": []
},
"cctv-provider": {
"runtime": "node",
"exports": [
"./server/providers/cctv"
],
"exports": ["./server/providers/cctv"],
"modules": [
"server/providers/cctv.js",
"server/providers/cctv/catalog.js",
@@ -220,9 +151,7 @@
},
"radio-provider": {
"runtime": "node",
"exports": [
"./server/providers/radio"
],
"exports": ["./server/providers/radio"],
"modules": [
"server/providers/radio.js",
"server/providers/radio/catalog.js",
@@ -236,9 +165,7 @@
},
"overpass-provider": {
"runtime": "node",
"exports": [
"./server/providers/overpass"
],
"exports": ["./server/providers/overpass"],
"modules": [
"server/providers/common/geo.js",
"server/providers/common/http.js",
@@ -257,9 +184,7 @@
},
"military-installations-provider": {
"runtime": "node",
"exports": [
"./server/providers/military-installations"
],
"exports": ["./server/providers/military-installations"],
"modules": [
"server/providers/common/http.js",
"server/providers/common/query.js",
@@ -276,9 +201,7 @@
},
"regional-provider": {
"runtime": "node",
"exports": [
"./server/providers/regional"
],
"exports": ["./server/providers/regional"],
"modules": [
"server/providers/common/http.js",
"server/providers/common/query.js",
@@ -297,9 +220,7 @@
},
"openai-provider": {
"runtime": "node",
"exports": [
"./server/providers/openai"
],
"exports": ["./server/providers/openai"],
"modules": [
"server/providers/common/rate-limit.js",
"server/providers/common/request.js",
@@ -319,9 +240,7 @@
},
"standalone-key-setup": {
"runtime": "node",
"exports": [
"./server/standalone/key-setup"
],
"exports": ["./server/standalone/key-setup"],
"modules": [
"scripts/pinokio-environment.mjs",
"server/providers/common/source-root.js",
-1
View File
@@ -1,4 +1,3 @@
/** Read a request body with a hard byte cap; throws { code:'BODY_TOO_LARGE' } past the cap. */
async function readRequestBodyCapped(req, maxBytes) {
const chunks = [];
+4 -1
View File
@@ -2,6 +2,9 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
/** Resolve defaultSourceRoot for ESM context. */
const defaultSourceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
const defaultSourceRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'../../..',
);
export { defaultSourceRoot };
+11 -2
View File
@@ -47,8 +47,17 @@ function localProviderPlugins() {
export { localProviderPlugins };
export { CCTV_FRAME_FETCH_TIMEOUT_MS, fetchCctvImageFromUpstream } from './cctv.js';
export { createRadioProxyMiddleware, isPublicRadioAddress, normalizeRadioBrowserStation, publicRadioStation, publicRadioHttpsUrl } from './radio.js';
export {
CCTV_FRAME_FETCH_TIMEOUT_MS,
fetchCctvImageFromUpstream,
} from './cctv.js';
export {
createRadioProxyMiddleware,
isPublicRadioAddress,
normalizeRadioBrowserStation,
publicRadioStation,
publicRadioHttpsUrl,
} from './radio.js';
export { LL2_CACHE_TTL_MS, launchLibraryRequestHeaders } from './space.js';
export { googlePlacesContextProxy } from './places.js';
export { googleServerApiKey } from './places.js';
+83 -20
View File
@@ -1,11 +1,31 @@
import { makeRateLimiter, clientKey } from './common/rate-limit.js';
import { MILITARY_INSTALLATION_ELEMENT_CAP, MILITARY_INSTALLATION_MAX_RESPONSE_BYTES, MILITARY_INSTALLATION_DISK_TTL_MS, MILITARY_INSTALLATION_STALE_MS } from './military-installations/constants.js';
import {
MILITARY_INSTALLATION_ELEMENT_CAP,
MILITARY_INSTALLATION_MAX_RESPONSE_BYTES,
MILITARY_INSTALLATION_DISK_TTL_MS,
MILITARY_INSTALLATION_STALE_MS,
} from './military-installations/constants.js';
import { fetchOverpassPayload } from './overpass/transport.js';
import { _militaryInstallationCache, trimMilitaryInstallationCache, writeMilitaryInstallationDisk, resolveMilitaryInstallationTier, readMilitaryInstallationDisk } from './military-installations/cache.js';
import { validMilitaryInstallationBox, quantizeMilitaryInstallationBox, militaryInstallationCacheKey, militaryInstallationFailureReason } from './military-installations/query.js';
import {
_militaryInstallationCache,
trimMilitaryInstallationCache,
writeMilitaryInstallationDisk,
resolveMilitaryInstallationTier,
readMilitaryInstallationDisk,
} from './military-installations/cache.js';
import {
validMilitaryInstallationBox,
quantizeMilitaryInstallationBox,
militaryInstallationCacheKey,
militaryInstallationFailureReason,
} from './military-installations/query.js';
import { coalesceProxyRequest } from './common/http.js';
const _militaryInstallationsRateLimiter = makeRateLimiter({ windowMs: 60_000, max: 90, globalMax: 300 });
const _militaryInstallationsRateLimiter = makeRateLimiter({
windowMs: 60_000,
max: 90,
globalMax: 300,
});
const _militaryInstallationInFlight = new Map();
@@ -17,12 +37,23 @@ function militaryInstallationsProxy() {
`data=${encodeURIComponent(ql)}`,
MILITARY_INSTALLATION_MAX_RESPONSE_BYTES,
);
if (upstream.status >= 400 || upstream.rateLimited || upstream.runtimeError) {
throw Object.assign(new Error('Mapped installation upstream unavailable'), {
installationReason: upstream.rateLimited ? 'rate_limited'
: upstream.status === 504 ? 'timeout'
: upstream.runtimeError ? 'query_failed' : 'unavailable',
});
if (
upstream.status >= 400 ||
upstream.rateLimited ||
upstream.runtimeError
) {
throw Object.assign(
new Error('Mapped installation upstream unavailable'),
{
installationReason: upstream.rateLimited
? 'rate_limited'
: upstream.status === 504
? 'timeout'
: upstream.runtimeError
? 'query_failed'
: 'unavailable',
},
);
}
const parsed = JSON.parse(upstream.body);
const elements = Array.isArray(parsed?.elements)
@@ -53,7 +84,10 @@ function militaryInstallationsProxy() {
return;
}
if (!_militaryInstallationsRateLimiter(clientKey(req))) {
res.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': '5' });
res.writeHead(429, {
'Content-Type': 'application/json',
'Retry-After': '5',
});
res.end(JSON.stringify({ error: 'Rate limit exceeded' }));
return;
}
@@ -61,7 +95,11 @@ function militaryInstallationsProxy() {
const requested = validMilitaryInstallationBox(url.searchParams);
if (!requested) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'A non-dateline bbox no larger than 10 degrees is required' }));
res.end(
JSON.stringify({
error: 'A non-dateline bbox no larger than 10 degrees is required',
}),
);
return;
}
// Query the SNAPPED box, not the raw viewport: neighbouring views then
@@ -70,7 +108,9 @@ function militaryInstallationsProxy() {
// response, so a truncated tile can never starve the actual viewport. It
// is keyed separately so exact and snapped answers never collide.
const exact = url.searchParams.get('exact') === '1';
const box = exact ? requested : quantizeMilitaryInstallationBox(requested);
const box = exact
? requested
: quantizeMilitaryInstallationBox(requested);
// Key at the precision the query actually uses (see militaryInstallationCacheKey).
const key = exact
? `exact:${militaryInstallationCacheKey(box, 5)}`
@@ -81,7 +121,8 @@ function militaryInstallationsProxy() {
cacheKey: key,
memoryCache: _militaryInstallationCache,
inFlight: _militaryInstallationInFlight,
readDisk: () => readMilitaryInstallationDisk(key, MILITARY_INSTALLATION_DISK_TTL_MS),
readDisk: () =>
readMilitaryInstallationDisk(key, MILITARY_INSTALLATION_DISK_TTL_MS),
now,
});
if (preflight.source !== 'UPSTREAM') {
@@ -89,8 +130,14 @@ function militaryInstallationsProxy() {
_militaryInstallationCache.set(key, preflight.entry);
trimMilitaryInstallationCache();
}
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=60', 'X-Military-Installations': preflight.source });
res.end(JSON.stringify({ ...preflight.entry.payload, status: 'cached' }));
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=60',
'X-Military-Installations': preflight.source,
});
res.end(
JSON.stringify({ ...preflight.entry.payload, status: 'cached' }),
);
return;
}
const request = coalesceProxyRequest(
@@ -108,7 +155,11 @@ function militaryInstallationsProxy() {
res.end(JSON.stringify(payload));
} catch (error) {
if (cached && now - cached.cachedAt <= MILITARY_INSTALLATION_STALE_MS) {
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', 'X-Military-Installations': 'STALE' });
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
'X-Military-Installations': 'STALE',
});
res.end(JSON.stringify({ ...cached.payload, status: 'stale' }));
return;
}
@@ -116,12 +167,24 @@ function militaryInstallationsProxy() {
// layer (the same serve-stale rule the Overpass proxy applies).
const stale = await readMilitaryInstallationDisk(key, Infinity);
if (stale) {
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', 'X-Military-Installations': 'STALE-DISK' });
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
'X-Military-Installations': 'STALE-DISK',
});
res.end(JSON.stringify({ ...stale.payload, status: 'stale' }));
return;
}
res.writeHead(503, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ error: 'Mapped installation context is temporarily unavailable', reason: militaryInstallationFailureReason(error) }));
res.writeHead(503, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
res.end(
JSON.stringify({
error: 'Mapped installation context is temporarily unavailable',
reason: militaryInstallationFailureReason(error),
}),
);
}
});
}
@@ -1,4 +1,10 @@
import { MILITARY_INSTALLATION_CACHE_MS, MILITARY_INSTALLATION_ELEMENT_CAP, MILITARY_INSTALLATION_DISK_TTL_MS, MILITARY_INSTALLATION_DISK_DIR, MILITARY_INSTALLATION_MAX_CACHE } from './constants.js';
import {
MILITARY_INSTALLATION_CACHE_MS,
MILITARY_INSTALLATION_ELEMENT_CAP,
MILITARY_INSTALLATION_DISK_TTL_MS,
MILITARY_INSTALLATION_DISK_DIR,
MILITARY_INSTALLATION_MAX_CACHE,
} from './constants.js';
import path from 'node:path';
import { createHash } from 'node:crypto';
import { promises as fsp } from 'node:fs';
@@ -32,10 +38,13 @@ async function resolveMilitaryInstallationTier({
cacheMs = MILITARY_INSTALLATION_CACHE_MS,
}) {
const cached = memoryCache.get(cacheKey);
if (cached && now - cached.cachedAt <= cacheMs) return { source: 'HIT', entry: cached };
if (cached && now - cached.cachedAt <= cacheMs)
return { source: 'HIT', entry: cached };
if (inFlight.has(cacheKey)) return { source: 'UPSTREAM', entry: null };
const disk = await readDisk();
return disk ? { source: 'DISK', entry: disk } : { source: 'UPSTREAM', entry: null };
return disk
? { source: 'DISK', entry: disk }
: { source: 'UPSTREAM', entry: null };
}
/**
@@ -51,8 +60,11 @@ async function resolveMilitaryInstallationTier({
* @returns {?{payload: object, cachedAt: number}}
*/
function migrateMilitaryInstallationEntry(entry) {
if (!entry?.payload || typeof entry.payload.saturated === 'boolean') return entry;
const elements = Array.isArray(entry.payload.elements) ? entry.payload.elements : [];
if (!entry?.payload || typeof entry.payload.saturated === 'boolean')
return entry;
const elements = Array.isArray(entry.payload.elements)
? entry.payload.elements
: [];
return {
...entry,
payload: {
@@ -68,13 +80,24 @@ function militaryInstallationDiskFresh(
maxAgeMs = MILITARY_INSTALLATION_DISK_TTL_MS,
now = Date.now(),
) {
if (!entry || !Number.isFinite(entry.cachedAt) || !Array.isArray(entry.payload?.elements)) return false;
if (
!entry ||
!Number.isFinite(entry.cachedAt) ||
!Array.isArray(entry.payload?.elements)
)
return false;
return now - entry.cachedAt <= maxAgeMs;
}
/** Cache key -> stable disk-cache file path. */
function militaryInstallationDiskPath(cacheKey, dir = MILITARY_INSTALLATION_DISK_DIR) {
return path.join(dir, `${createHash('sha1').update(cacheKey).digest('hex')}.json`);
function militaryInstallationDiskPath(
cacheKey,
dir = MILITARY_INSTALLATION_DISK_DIR,
) {
return path.join(
dir,
`${createHash('sha1').update(cacheKey).digest('hex')}.json`,
);
}
/**
@@ -88,7 +111,9 @@ async function readMilitaryInstallationDisk(
dir = MILITARY_INSTALLATION_DISK_DIR,
) {
try {
const entry = JSON.parse(await fsp.readFile(militaryInstallationDiskPath(cacheKey, dir), 'utf8'));
const entry = JSON.parse(
await fsp.readFile(militaryInstallationDiskPath(cacheKey, dir), 'utf8'),
);
if (!militaryInstallationDiskFresh(entry, maxAgeMs)) return null;
return migrateMilitaryInstallationEntry(entry);
} catch {
@@ -117,7 +142,10 @@ async function writeMilitaryInstallationDisk(
await fsp.rename(temp, target);
return true;
} catch (err) {
console.warn('[Installations Proxy] disk cache write failed:', err?.message || err);
console.warn(
'[Installations Proxy] disk cache write failed:',
err?.message || err,
);
await fsp.rm(temp, { force: true }).catch(() => {});
return false;
}
@@ -131,4 +159,13 @@ function trimMilitaryInstallationCache() {
}
}
export { _militaryInstallationCache, trimMilitaryInstallationCache, writeMilitaryInstallationDisk, resolveMilitaryInstallationTier, readMilitaryInstallationDisk, migrateMilitaryInstallationEntry, militaryInstallationDiskFresh, militaryInstallationDiskPath };
export {
_militaryInstallationCache,
trimMilitaryInstallationCache,
writeMilitaryInstallationDisk,
resolveMilitaryInstallationTier,
readMilitaryInstallationDisk,
migrateMilitaryInstallationEntry,
militaryInstallationDiskFresh,
militaryInstallationDiskPath,
};
@@ -33,7 +33,11 @@ const MILITARY_INSTALLATION_ELEMENT_CAP = 700;
const MILITARY_INSTALLATION_DISK_TTL_MS = 30 * 86_400_000;
/** Disk-cache directory for mapped installation payloads. */
const MILITARY_INSTALLATION_DISK_DIR = path.join(process.cwd(), '.gev-cache', 'military-installations');
const MILITARY_INSTALLATION_DISK_DIR = path.join(
process.cwd(),
'.gev-cache',
'military-installations',
);
/**
* Cache-key grid step in degrees (~5.5 km).
@@ -45,4 +49,13 @@ const MILITARY_INSTALLATION_DISK_DIR = path.join(process.cwd(), '.gev-cache', 'm
*/
const MILITARY_INSTALLATION_BBOX_STEP_DEG = 0.05;
export { MILITARY_INSTALLATION_ELEMENT_CAP, MILITARY_INSTALLATION_MAX_RESPONSE_BYTES, MILITARY_INSTALLATION_DISK_TTL_MS, MILITARY_INSTALLATION_STALE_MS, MILITARY_INSTALLATION_CACHE_MS, MILITARY_INSTALLATION_DISK_DIR, MILITARY_INSTALLATION_MAX_CACHE, MILITARY_INSTALLATION_BBOX_STEP_DEG };
export {
MILITARY_INSTALLATION_ELEMENT_CAP,
MILITARY_INSTALLATION_MAX_RESPONSE_BYTES,
MILITARY_INSTALLATION_DISK_TTL_MS,
MILITARY_INSTALLATION_STALE_MS,
MILITARY_INSTALLATION_CACHE_MS,
MILITARY_INSTALLATION_DISK_DIR,
MILITARY_INSTALLATION_MAX_CACHE,
MILITARY_INSTALLATION_BBOX_STEP_DEG,
};
@@ -7,12 +7,17 @@ import { requiredFiniteQueryNumber } from '../common/query.js';
* @param {number} [stepDeg]
* @returns {{south:number, west:number, north:number, east:number}}
*/
function quantizeMilitaryInstallationBox(box, stepDeg = MILITARY_INSTALLATION_BBOX_STEP_DEG) {
function quantizeMilitaryInstallationBox(
box,
stepDeg = MILITARY_INSTALLATION_BBOX_STEP_DEG,
) {
// Round the ratio first: 29.9999/0.05 lands a hair under an exact grid line
// in binary floating point, which would otherwise snap a whole cell too far.
const snap = (value, grow) => {
const cells = Number((value / stepDeg).toFixed(9));
return Number(((grow > 0 ? Math.ceil(cells) : Math.floor(cells)) * stepDeg).toFixed(6));
return Number(
((grow > 0 ? Math.ceil(cells) : Math.floor(cells)) * stepDeg).toFixed(6),
);
};
return {
south: Math.max(-90, snap(box.south, -1)),
@@ -46,15 +51,35 @@ function validMilitaryInstallationBox(params) {
const north = requiredFiniteQueryNumber(params, 'north');
const east = requiredFiniteQueryNumber(params, 'east');
if (![south, west, north, east].every(Number.isFinite)) return null;
if (south < -90 || north > 90 || west < -180 || east > 180 || south >= north || west >= east) return null;
if (
south < -90 ||
north > 90 ||
west < -180 ||
east > 180 ||
south >= north ||
west >= east
)
return null;
if (north - south > 10 || east - west > 10) return null;
return { south, west, north, east };
}
/** Safe, evidence-based reason for an installation upstream failure. */
function militaryInstallationFailureReason(error) {
if (['rate_limited', 'timeout', 'query_failed'].includes(error?.installationReason)) return error.installationReason;
return ['AbortError', 'TimeoutError'].includes(error?.name) ? 'timeout' : 'unavailable';
if (
['rate_limited', 'timeout', 'query_failed'].includes(
error?.installationReason,
)
)
return error.installationReason;
return ['AbortError', 'TimeoutError'].includes(error?.name)
? 'timeout'
: 'unavailable';
}
export { validMilitaryInstallationBox, quantizeMilitaryInstallationBox, militaryInstallationCacheKey, militaryInstallationFailureReason };
export {
validMilitaryInstallationBox,
quantizeMilitaryInstallationBox,
militaryInstallationCacheKey,
militaryInstallationFailureReason,
};
+12 -3
View File
@@ -9,13 +9,22 @@ import { createRealtimeTokenHandler } from './openai/realtime.js';
* Keeps OPENAI_API_KEY server-side while the browser connects to the
* Realtime API over WebRTC with a short-lived secret.
*/
function openAiRealtimeProxy({ sourceRoot = defaultSourceRoot, annotationGuidance } = {}) {
function openAiRealtimeProxy({
sourceRoot = defaultSourceRoot,
annotationGuidance,
} = {}) {
function install(middlewares) {
middlewares.use('/api/openai/hud-summary', handleHudSummary);
middlewares.use('/api/realtime/debug-log', createDebugLogHandler({ sourceRoot }));
middlewares.use(
'/api/realtime/debug-log',
createDebugLogHandler({ sourceRoot }),
);
middlewares.use('/api/realtime/token', createRealtimeTokenHandler({ annotationGuidance }));
middlewares.use(
'/api/realtime/token',
createRealtimeTokenHandler({ annotationGuidance }),
);
}
return {
+9 -1
View File
@@ -16,4 +16,12 @@ const OPENAI_REALTIME_CONTEXT_RETENTION_DEFAULT = 0.5;
const OPENAI_HUD_SUMMARY_MODEL_DEFAULT = 'gpt-5-nano';
export { OPENAI_HUD_SUMMARY_MODEL_DEFAULT, OPENAI_REALTIME_MODEL_MINI_DEFAULT, OPENAI_REALTIME_MODEL_DEFAULT, OPENAI_REALTIME_VOICE_DEFAULT, OPENAI_REALTIME_REASONING_DEFAULT, OPENAI_REALTIME_CONTEXT_TOKENS_DEFAULT, OPENAI_REALTIME_CONTEXT_RETENTION_DEFAULT };
export {
OPENAI_HUD_SUMMARY_MODEL_DEFAULT,
OPENAI_REALTIME_MODEL_MINI_DEFAULT,
OPENAI_REALTIME_MODEL_DEFAULT,
OPENAI_REALTIME_VOICE_DEFAULT,
OPENAI_REALTIME_REASONING_DEFAULT,
OPENAI_REALTIME_CONTEXT_TOKENS_DEFAULT,
OPENAI_REALTIME_CONTEXT_RETENTION_DEFAULT,
};
+16 -6
View File
@@ -6,9 +6,12 @@ import fs from 'node:fs';
const REALTIME_DEBUG_LOG_MAX_BYTES = 8 * 1024 * 1024;
function createDebugLogHandler({ sourceRoot = defaultSourceRoot } = {}) {
const REALTIME_DEBUG_LOG_DIR = path.join(sourceRoot, '.gev-logs');
const REALTIME_DEBUG_LOG_FILE = path.join(REALTIME_DEBUG_LOG_DIR, 'realtime-conversations.jsonl');
return async (req, res) => {
const REALTIME_DEBUG_LOG_DIR = path.join(sourceRoot, '.gev-logs');
const REALTIME_DEBUG_LOG_FILE = path.join(
REALTIME_DEBUG_LOG_DIR,
'realtime-conversations.jsonl',
);
return async (req, res) => {
if (req.method !== 'POST') {
res.statusCode = 405;
res.setHeader('Content-Type', 'application/json');
@@ -20,16 +23,23 @@ return async (req, res) => {
const body = await readRequestBody(req, REALTIME_DEBUG_LOG_MAX_BYTES);
const record = JSON.parse(body || '{}');
fs.mkdirSync(REALTIME_DEBUG_LOG_DIR, { recursive: true });
fs.appendFileSync(REALTIME_DEBUG_LOG_FILE, `${JSON.stringify({
fs.appendFileSync(
REALTIME_DEBUG_LOG_FILE,
`${JSON.stringify({
loggedAt: new Date().toISOString(),
...record,
})}\n`);
})}\n`,
);
res.statusCode = 204;
res.end();
} catch (error) {
res.statusCode = 400;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: error?.message || 'Failed to write Realtime debug log' }));
res.end(
JSON.stringify({
error: error?.message || 'Failed to write Realtime debug log',
}),
);
}
};
}
+17 -7
View File
@@ -9,7 +9,7 @@ function extractOpenAiResponseText(data) {
}
if (!Array.isArray(data?.output)) return '';
return data.output
.flatMap((item) => Array.isArray(item?.content) ? item.content : [])
.flatMap((item) => (Array.isArray(item?.content) ? item.content : []))
.map((part) => part?.text || part?.output_text || '')
.join(' ')
.trim();
@@ -58,7 +58,9 @@ async function handleHudSummary(req, res) {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: process.env.OPENAI_HUD_SUMMARY_MODEL || OPENAI_HUD_SUMMARY_MODEL_DEFAULT,
model:
process.env.OPENAI_HUD_SUMMARY_MODEL ||
OPENAI_HUD_SUMMARY_MODEL_DEFAULT,
instructions: [
"Write one concise intelligence-HUD summary for God's Eye View.",
'Use only the supplied place, street, nearby-place, and enabled-layer text labels.',
@@ -76,15 +78,23 @@ async function handleHudSummary(req, res) {
res.statusCode = response.ok && summary ? 200 : response.status || 502;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Cache-Control', 'no-store');
res.end(JSON.stringify({
res.end(
JSON.stringify({
summary: summary || null,
error: response.ok ? null : data.error?.message || 'OpenAI HUD summary request failed',
}));
error: response.ok
? null
: data.error?.message || 'OpenAI HUD summary request failed',
}),
);
} catch (error) {
res.statusCode = 502;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: error?.message || 'OpenAI HUD summary request failed' }));
}
res.end(
JSON.stringify({
error: error?.message || 'OpenAI HUD summary request failed',
}),
);
}
}
export { handleHudSummary };
+6 -4
View File
@@ -1,8 +1,10 @@
const DEFAULT_ANNOTATION_GUIDANCE =
'Use a single annotate_map call with several annotations when you are describing multiple related places at once. Set flyTo true only when the user is not already looking at the place; if every mark in a call lands off-screen the app auto-frames them, so when unsure leave flyTo false. Do NOT say out loud that you are drawing, highlighting, or annotating — just speak naturally about the places while the marks appear. ANNOTATIONS ACCUMULATE AND PERSIST — keep adding marks as you explore; you can fly around, change topic, and jump between far-apart places and the marks STAY, so the user can build up the map and show people things. Do NOT clear on your own initiative: never pass clearPrevious, and call clear_annotations ONLY when the user EXPLICITLY asks to clear or reset the map.';
const DEFAULT_ANNOTATION_GUIDANCE = 'Use a single annotate_map call with several annotations when you are describing multiple related places at once. Set flyTo true only when the user is not already looking at the place; if every mark in a call lands off-screen the app auto-frames them, so when unsure leave flyTo false. Do NOT say out loud that you are drawing, highlighting, or annotating — just speak naturally about the places while the marks appear. ANNOTATIONS ACCUMULATE AND PERSIST — keep adding marks as you explore; you can fly around, change topic, and jump between far-apart places and the marks STAY, so the user can build up the map and show people things. Do NOT clear on your own initiative: never pass clearPrevious, and call clear_annotations ONLY when the user EXPLICITLY asks to clear or reset the map.';
function realtimeInstructions(annotationGuidance = DEFAULT_ANNOTATION_GUIDANCE) {
return [
function realtimeInstructions(
annotationGuidance = DEFAULT_ANNOTATION_GUIDANCE,
) {
return [
"You are GEV Voice Control, a concise voice controller for a Cesium geospatial app called God's Eye View.",
'Have a natural spoken conversation with the user while the mic session is active.',
'Do not require a wake phrase. Treat direct commands like "zoom into London" or "open datacenters" as GEV control requests.',
+4 -1
View File
@@ -9,7 +9,10 @@ let _openAiRateLimiter;
/** OpenAI cost endpoints (realtime/token + hud-summary). Null = unlimited (default). */
function openAiRateLimiter() {
if (_openAiRateLimiter === undefined) _openAiRateLimiter = makeOptInRateLimiter(process.env.GEV_RATELIMIT_OPENAI_PER_MIN);
if (_openAiRateLimiter === undefined)
_openAiRateLimiter = makeOptInRateLimiter(
process.env.GEV_RATELIMIT_OPENAI_PER_MIN,
);
return _openAiRateLimiter;
}
+51 -15
View File
@@ -1,11 +1,21 @@
import { enforceOptInRateLimit, openAiRateLimiter } from './rate-limit.js';
import { resolveVoiceModel, isKnownVoiceTier } from '../../../src/voice/voiceCost.js';
import { OPENAI_REALTIME_MODEL_MINI_DEFAULT, OPENAI_REALTIME_MODEL_DEFAULT, OPENAI_REALTIME_VOICE_DEFAULT, OPENAI_REALTIME_REASONING_DEFAULT, OPENAI_REALTIME_CONTEXT_TOKENS_DEFAULT, OPENAI_REALTIME_CONTEXT_RETENTION_DEFAULT } from './constants.js';
import {
resolveVoiceModel,
isKnownVoiceTier,
} from '../../../src/voice/voiceCost.js';
import {
OPENAI_REALTIME_MODEL_MINI_DEFAULT,
OPENAI_REALTIME_MODEL_DEFAULT,
OPENAI_REALTIME_VOICE_DEFAULT,
OPENAI_REALTIME_REASONING_DEFAULT,
OPENAI_REALTIME_CONTEXT_TOKENS_DEFAULT,
OPENAI_REALTIME_CONTEXT_RETENTION_DEFAULT,
} from './constants.js';
import { realtimeInstructions } from './instructions.js';
import { GEV_REALTIME_TOOLS } from './tools.js';
function createRealtimeTokenHandler({ annotationGuidance } = {}) {
return async (req, res) => {
return async (req, res) => {
if (req.method !== 'GET' && req.method !== 'POST') {
res.statusCode = 405;
res.setHeader('Content-Type', 'application/json');
@@ -32,7 +42,9 @@ return async (req, res) => {
// a wrong upstream model id is then a config fix, not a code change.
const requestedTier = (() => {
try {
return new URL(req.url || '', 'http://localhost').searchParams.get('tier');
return new URL(req.url || '', 'http://localhost').searchParams.get(
'tier',
);
} catch {
return null;
}
@@ -40,17 +52,31 @@ return async (req, res) => {
const tier = resolveVoiceModel(requestedTier).tier;
const model =
tier === 'mini'
? process.env.OPENAI_REALTIME_MODEL_MINI || OPENAI_REALTIME_MODEL_MINI_DEFAULT
? process.env.OPENAI_REALTIME_MODEL_MINI ||
OPENAI_REALTIME_MODEL_MINI_DEFAULT
: process.env.OPENAI_REALTIME_MODEL || OPENAI_REALTIME_MODEL_DEFAULT;
const voice = process.env.OPENAI_REALTIME_VOICE || OPENAI_REALTIME_VOICE_DEFAULT;
const effort = process.env.OPENAI_REALTIME_REASONING_EFFORT || OPENAI_REALTIME_REASONING_DEFAULT;
const contextTokenLimit = Math.round(Math.max(
const voice =
process.env.OPENAI_REALTIME_VOICE || OPENAI_REALTIME_VOICE_DEFAULT;
const effort =
process.env.OPENAI_REALTIME_REASONING_EFFORT ||
OPENAI_REALTIME_REASONING_DEFAULT;
const contextTokenLimit = Math.round(
Math.max(
1000,
Math.min(12000, Number(process.env.OPENAI_REALTIME_CONTEXT_TOKENS) || OPENAI_REALTIME_CONTEXT_TOKENS_DEFAULT)
));
Math.min(
12000,
Number(process.env.OPENAI_REALTIME_CONTEXT_TOKENS) ||
OPENAI_REALTIME_CONTEXT_TOKENS_DEFAULT,
),
),
);
const contextRetentionRatio = Math.max(
0.1,
Math.min(1, Number(process.env.OPENAI_REALTIME_CONTEXT_RETENTION) || OPENAI_REALTIME_CONTEXT_RETENTION_DEFAULT)
Math.min(
1,
Number(process.env.OPENAI_REALTIME_CONTEXT_RETENTION) ||
OPENAI_REALTIME_CONTEXT_RETENTION_DEFAULT,
),
);
const sessionConfig = {
session: {
@@ -83,7 +109,9 @@ return async (req, res) => {
};
try {
const response = await fetch('https://api.openai.com/v1/realtime/client_secrets', {
const response = await fetch(
'https://api.openai.com/v1/realtime/client_secrets',
{
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
@@ -91,10 +119,14 @@ return async (req, res) => {
'OpenAI-Safety-Identifier': 'gev-local-dev',
},
body: JSON.stringify(sessionConfig),
});
},
);
const body = await response.text();
res.statusCode = response.status;
res.setHeader('Content-Type', response.headers.get('content-type') || 'application/json');
res.setHeader(
'Content-Type',
response.headers.get('content-type') || 'application/json',
);
// Which tier/model this secret was actually minted for. The upstream
// body is passed through untouched (the client parses it verbatim), so
// these headers are the authoritative echo — including the case where a
@@ -108,7 +140,11 @@ return async (req, res) => {
} catch (error) {
res.statusCode = 502;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: error?.message || 'Failed to create Realtime token' }));
res.end(
JSON.stringify({
error: error?.message || 'Failed to create Realtime token',
}),
);
}
};
}
+455 -105
View File
@@ -1,38 +1,52 @@
const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'fly_to_location',
description: "Fly the God's Eye View camera to a known city, geocoded country/region/city/landmark, or explicit WGS84 coordinate. Countries/cities frame the whole place; landmarks/buildings use close framing.",
description:
"Fly the God's Eye View camera to a known city, geocoded country/region/city/landmark, or explicit WGS84 coordinate. Countries/cities frame the whole place; landmarks/buildings use close framing.",
parameters: {
type: 'object',
additionalProperties: false,
properties: {
locationId: {
type: 'string',
enum: ['austin', 'sf', 'nyc', 'tokyo', 'london', 'paris', 'dubai', 'dc'],
description: 'Known city preset ID. Use when the requested place matches one of these cities.',
enum: [
'austin',
'sf',
'nyc',
'tokyo',
'london',
'paris',
'dubai',
'dc',
],
description:
'Known city preset ID. Use when the requested place matches one of these cities.',
},
query: {
type: 'string',
description: 'Plain place search query, e.g. "London", "Eiffel Tower", or "Dubai Marina".',
description:
'Plain place search query, e.g. "London", "Eiffel Tower", or "Dubai Marina".',
},
latitude: { type: 'number', minimum: -90, maximum: 90 },
longitude: { type: 'number', minimum: -180, maximum: 180 },
viewMode: {
type: 'string',
enum: ['close', 'overview'],
description: 'Optional framing intent. Usually omit this; GEV infers whole-place framing for countries/cities and close framing for landmarks.',
description:
'Optional framing intent. Usually omit this; GEV infers whole-place framing for countries/cities and close framing for landmarks.',
},
rangeM: {
type: 'number',
minimum: 100,
maximum: 20000000,
description: 'Optional camera range from the target in meters. Omit it for automatic whole-country/whole-city or close-landmark framing; provide it only when the user explicitly requests a numeric height or distance.',
description:
'Optional camera range from the target in meters. Omit it for automatic whole-country/whole-city or close-landmark framing; provide it only when the user explicitly requests a numeric height or distance.',
},
waitForArrival: {
type: 'boolean',
description: 'Set true when a later tool depends on the destination viewport. The result then waits for the camera flight and returns arrived=true; cancellation returns ok=false.',
description:
'Set true when a later tool depends on the destination viewport. The result then waits for the camera flight and returns arrived=true; cancellation returns ok=false.',
},
},
},
@@ -40,7 +54,8 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'select_nearest_aircraft',
description: 'Atomically fly to a place, wait for arrival, enable and load Flights or Military Flights in that viewport, exclude on-ground records, and select/follow the nearest airborne aircraft. Healthy fallback feeds remain usable and are reported in the result. This does not open Contacts or Cockpit.',
description:
'Atomically fly to a place, wait for arrival, enable and load Flights or Military Flights in that viewport, exclude on-ground records, and select/follow the nearest airborne aircraft. Healthy fallback feeds remain usable and are reported in the result. This does not open Contacts or Cockpit.',
parameters: {
type: 'object',
additionalProperties: false,
@@ -48,12 +63,23 @@ const GEV_REALTIME_TOOLS = [
layerId: {
type: 'string',
enum: ['flights', 'military'],
description: 'Aircraft layer to enable and search. Use flights unless the user explicitly asks for military aircraft.',
description:
'Aircraft layer to enable and search. Use flights unless the user explicitly asks for military aircraft.',
},
locationId: {
type: 'string',
enum: ['austin', 'sf', 'nyc', 'tokyo', 'london', 'paris', 'dubai', 'dc'],
description: 'Known city preset ID when the place matches one of these cities.',
enum: [
'austin',
'sf',
'nyc',
'tokyo',
'london',
'paris',
'dubai',
'dc',
],
description:
'Known city preset ID when the place matches one of these cities.',
},
locationQuery: {
type: 'string',
@@ -69,7 +95,8 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'adjust_camera_zoom',
description: 'Move the current Cesium camera closer to or farther from what it is presently looking at. Use for relative zoom requests without changing location.',
description:
'Move the current Cesium camera closer to or farther from what it is presently looking at. Use for relative zoom requests without changing location.',
parameters: {
type: 'object',
additionalProperties: false,
@@ -81,7 +108,8 @@ const GEV_REALTIME_TOOLS = [
amount: {
type: 'string',
enum: ['little', 'medium', 'lot'],
description: 'Use little for phrases like "a bit" or "a little", medium for ordinary zoom requests, and lot for "way out/in".',
description:
'Use little for phrases like "a bit" or "a little", medium for ordinary zoom requests, and lot for "way out/in".',
},
},
required: ['direction', 'amount'],
@@ -90,7 +118,8 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'zoom_to_globe',
description: 'Pull the camera out to an ABSOLUTE full-Earth globe view (~18,000 km altitude, the whole planet in frame), keeping the current region centered. Use for "globe view", "whole earth", "see the planet", "zoom all the way out". Never use adjust_camera_zoom for these — its relative steps cannot reach the globe.',
description:
'Pull the camera out to an ABSOLUTE full-Earth globe view (~18,000 km altitude, the whole planet in frame), keeping the current region centered. Use for "globe view", "whole earth", "see the planet", "zoom all the way out". Never use adjust_camera_zoom for these — its relative steps cannot reach the globe.',
parameters: {
type: 'object',
additionalProperties: false,
@@ -134,7 +163,8 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'show_data_layers_menu',
description: 'Open the data layers dropdown/menu and optionally scroll to a specific layer row without toggling it.',
description:
'Open the data layers dropdown/menu and optionally scroll to a specific layer row without toggling it.',
parameters: {
type: 'object',
additionalProperties: false,
@@ -171,7 +201,16 @@ const GEV_REALTIME_TOOLS = [
properties: {
panelId: {
type: 'string',
enum: ['data-panel', 'location-bar', 'control-panel', 'cctv-panel', 'radio-panel', 'scene-panel', 'pp-toggles', 'global-context-panel'],
enum: [
'data-panel',
'location-bar',
'control-panel',
'cctv-panel',
'radio-panel',
'scene-panel',
'pp-toggles',
'global-context-panel',
],
},
open: { type: 'boolean' },
},
@@ -181,7 +220,8 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'set_context_mode',
description: 'Enter or exit the Global Context sub-mode used by Contacts and Space Missions. Use Contacts only when the user explicitly requests Contacts, and Space Missions only when explicitly requested. A request to open the parent Context panel alone uses set_panel_open and must not activate either sub-mode. Selecting an aircraft does not imply Context.',
description:
'Enter or exit the Global Context sub-mode used by Contacts and Space Missions. Use Contacts only when the user explicitly requests Contacts, and Space Missions only when explicitly requested. A request to open the parent Context panel alone uses set_panel_open and must not activate either sub-mode. Selecting an aircraft does not imply Context.',
parameters: {
type: 'object',
additionalProperties: false,
@@ -198,7 +238,8 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'control_cockpit',
description: 'Read or control Cockpit when the user explicitly requests Cockpit: establish Contacts and enter from a selected or tracked aircraft; exit; or navigate nearby Contacts with optional filters. Selecting or viewing an aircraft alone must not enter Cockpit.',
description:
'Read or control Cockpit when the user explicitly requests Cockpit: establish Contacts and enter from a selected or tracked aircraft; exit; or navigate nearby Contacts with optional filters. Selecting or viewing an aircraft alone must not enter Cockpit.',
parameters: {
type: 'object',
additionalProperties: false,
@@ -206,16 +247,24 @@ const GEV_REALTIME_TOOLS = [
action: {
type: 'string',
enum: ['enter', 'exit', 'previous', 'next', 'prev', 'status'],
description: 'previous/next (or prev) navigates through nearby contacts in Cockpit context.',
description:
'previous/next (or prev) navigates through nearby contacts in Cockpit context.',
},
targetLayer: {
type: 'string',
enum: ['flights', 'military', 'ais-live-vessels', 'military-installations'],
description: 'Optional contact layer filter for next/previous (for example military for a military-only cycle).',
enum: [
'flights',
'military',
'ais-live-vessels',
'military-installations',
],
description:
'Optional contact layer filter for next/previous (for example military for a military-only cycle).',
},
aircraftClass: {
type: 'string',
description: 'Optional aircraft class filter (for example helicopter) when using next/previous navigation.',
description:
'Optional aircraft class filter (for example helicopter) when using next/previous navigation.',
},
},
required: ['action'],
@@ -231,7 +280,15 @@ const GEV_REALTIME_TOOLS = [
properties: {
style: {
type: 'string',
enum: ['normal', 'retro', 'surveillance', 'thermal', 'anime', 'noir', 'snow'],
enum: [
'normal',
'retro',
'surveillance',
'thermal',
'anime',
'noir',
'snow',
],
},
},
required: ['style'],
@@ -240,7 +297,8 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'get_entity_context',
description: 'Get current GEV scene context, including basemap/3D-tile target context, selected entity metadata if active, and entities currently visible in the camera view.',
description:
'Get current GEV scene context, including basemap/3D-tile target context, selected entity metadata if active, and entities currently visible in the camera view.',
parameters: {
type: 'object',
additionalProperties: false,
@@ -248,7 +306,8 @@ const GEV_REALTIME_TOOLS = [
scope: {
type: 'string',
enum: ['auto', 'selected', 'in_view'],
description: 'Use auto by default. selected returns the clicked/selected entity; in_view returns visible entities near the screen center.',
description:
'Use auto by default. selected returns the clicked/selected entity; in_view returns visible entities near the screen center.',
},
layerId: {
type: 'string',
@@ -271,7 +330,8 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'get_current_view_state',
description: 'Read the current camera, style, Context, Cockpit, HUD, detection, map stack, post-processing, scene-playback, tracked-entity, and layer state before choosing another action.',
description:
'Read the current camera, style, Context, Cockpit, HUD, detection, map stack, post-processing, scene-playback, tracked-entity, and layer state before choosing another action.',
parameters: {
type: 'object',
additionalProperties: false,
@@ -281,12 +341,17 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'set_hud',
description: 'Control the intelligence HUD overlay: visibility and/or layout variant.',
description:
'Control the intelligence HUD overlay: visibility and/or layout variant.',
parameters: {
type: 'object',
additionalProperties: false,
properties: {
visible: { type: 'string', enum: ['on', 'off', 'auto'], description: 'auto restores style-driven show/hide.' },
visible: {
type: 'string',
enum: ['on', 'off', 'auto'],
description: 'auto restores style-driven show/hide.',
},
layout: { type: 'string', enum: ['tactical', 'operator', 'minimal'] },
},
},
@@ -294,30 +359,52 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'set_detection',
description: 'Control the detection overlay: on/off, density-derived Sparse/Balanced/Dense profile, and Elastic/Weighted layer allocation.',
description:
'Control the detection overlay: on/off, density-derived Sparse/Balanced/Dense profile, and Elastic/Weighted layer allocation.',
parameters: {
type: 'object',
additionalProperties: false,
properties: {
enabled: { type: 'boolean', description: 'false turns detection OFF; true restores the current density-derived profile.' },
enabled: {
type: 'boolean',
description:
'false turns detection OFF; true restores the current density-derived profile.',
},
mode: { type: 'string', enum: ['sparse', 'balanced', 'dense'] },
densityPct: { type: 'number', description: 'Density snaps to 0, 25, 50, 75, or 100 and derives the active profile.' },
allocationStrategy: { type: 'string', enum: ['elastic', 'weighted'], description: 'Elastic splits evenly then lends unused slots; Weighted follows demand and semantic weight.' },
densityPct: {
type: 'number',
description:
'Density snaps to 0, 25, 50, 75, or 100 and derives the active profile.',
},
allocationStrategy: {
type: 'string',
enum: ['elastic', 'weighted'],
description:
'Elastic splits evenly then lends unused slots; Weighted follows demand and semantic weight.',
},
},
},
},
{
type: 'function',
name: 'set_map_stack',
description: 'Switch the basemap/imagery stack (NOT the satellites data layer and NOT a visual style filter).',
description:
'Switch the basemap/imagery stack (NOT the satellites data layer and NOT a visual style filter).',
parameters: {
type: 'object',
additionalProperties: false,
properties: {
stack: {
type: 'string',
enum: ['photoreal', 'bing-aerial', 'bing-labels', 'esri-imagery', 'osm'],
description: 'photoreal = Google 3D. Use bing-aerial only when the user explicitly says "Bing aerial" — "satellite(s)" never means a basemap; only the explicit phrase "Esri" / "Esri imagery" means esri-imagery.',
enum: [
'photoreal',
'bing-aerial',
'bing-labels',
'esri-imagery',
'osm',
],
description:
'photoreal = Google 3D. Use bing-aerial only when the user explicitly says "Bing aerial" — "satellite(s)" never means a basemap; only the explicit phrase "Esri" / "Esri imagery" means esri-imagery.',
},
},
required: ['stack'],
@@ -326,7 +413,8 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'set_post_processing',
description: 'Control bloom and sharpen post-processing toggles and intensities.',
description:
'Control bloom and sharpen post-processing toggles and intensities.',
parameters: {
type: 'object',
additionalProperties: false,
@@ -336,7 +424,10 @@ const GEV_REALTIME_TOOLS = [
additionalProperties: false,
properties: {
enabled: { type: 'boolean' },
intensityPct: { type: 'number', description: '0-200 (UI percent).' },
intensityPct: {
type: 'number',
description: '0-200 (UI percent).',
},
},
},
sharpen: {
@@ -344,7 +435,10 @@ const GEV_REALTIME_TOOLS = [
additionalProperties: false,
properties: {
enabled: { type: 'boolean' },
intensityPct: { type: 'number', description: '0-100 (UI percent).' },
intensityPct: {
type: 'number',
description: '0-100 (UI percent).',
},
},
},
},
@@ -353,13 +447,20 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'control_scene',
description: 'Cinematic scene playback: list scenes, play one scene by name, stop, advance, or read status. Play starts a single named scene and returns immediately.',
description:
'Cinematic scene playback: list scenes, play one scene by name, stop, advance, or read status. Play starts a single named scene and returns immediately.',
parameters: {
type: 'object',
additionalProperties: false,
properties: {
action: { type: 'string', enum: ['list', 'play', 'stop', 'next', 'status'] },
sceneId: { type: 'string', description: 'Scene id or (partial) title for play.' },
action: {
type: 'string',
enum: ['list', 'play', 'stop', 'next', 'status'],
},
sceneId: {
type: 'string',
description: 'Scene id or (partial) title for play.',
},
},
required: ['action'],
},
@@ -367,14 +468,38 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'control_cctv',
description: 'CCTV camera operations: enable/disable the layer, select a camera by name, next/prev/nearest/focus, toggle coverage wedges / projection overlay / auto-hop, "viewshed" for color-coded per-camera coverage volumes, and "adjust" for the on-camera calibration gizmo.',
description:
'CCTV camera operations: enable/disable the layer, select a camera by name, next/prev/nearest/focus, toggle coverage wedges / projection overlay / auto-hop, "viewshed" for color-coded per-camera coverage volumes, and "adjust" for the on-camera calibration gizmo.',
parameters: {
type: 'object',
additionalProperties: false,
properties: {
action: { type: 'string', enum: ['enable', 'disable', 'select', 'next', 'prev', 'nearest', 'focus', 'coverage', 'viewshed', 'adjust', 'projection', 'autohop'] },
cameraQuery: { type: 'string', description: 'Camera name or id for select.' },
enabled: { type: 'boolean', description: 'Explicit on/off for coverage/viewshed/adjust/projection/autohop; omit to toggle.' },
action: {
type: 'string',
enum: [
'enable',
'disable',
'select',
'next',
'prev',
'nearest',
'focus',
'coverage',
'viewshed',
'adjust',
'projection',
'autohop',
],
},
cameraQuery: {
type: 'string',
description: 'Camera name or id for select.',
},
enabled: {
type: 'boolean',
description:
'Explicit on/off for coverage/viewshed/adjust/projection/autohop; omit to toggle.',
},
},
required: ['action'],
},
@@ -382,32 +507,85 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'control_radio',
description: 'Control Internet Radio playback without moving the map. Use select whenever the request includes a station category, name, country, coordinates, or nearby place—even when the user says play. Use play only for an unqualified "turn on/start the radio" request so the current or nearest station begins. Enable only reveals the Radio layer/markers without audio. Also supports disable, resume, pause, stop, next/previous, volume, and status.',
description:
'Control Internet Radio playback without moving the map. Use select whenever the request includes a station category, name, country, coordinates, or nearby place—even when the user says play. Use play only for an unqualified "turn on/start the radio" request so the current or nearest station begins. Enable only reveals the Radio layer/markers without audio. Also supports disable, resume, pause, stop, next/previous, volume, and status.',
parameters: {
type: 'object',
additionalProperties: false,
properties: {
action: {
type: 'string',
enum: ['enable', 'disable', 'play', 'resume', 'pause', 'stop', 'next', 'previous', 'volume', 'select', 'status'],
description: 'Use select for any request qualified by category, station, country, coordinates, or place. Use play only for an unqualified turn on/start/listen request. Use enable only when the user explicitly asks to show or enable the Radio layer or its markers without requesting audio.',
enum: [
'enable',
'disable',
'play',
'resume',
'pause',
'stop',
'next',
'previous',
'volume',
'select',
'status',
],
description:
'Use select for any request qualified by category, station, country, coordinates, or place. Use play only for an unqualified turn on/start/listen request. Use enable only when the user explicitly asks to show or enable the Radio layer or its markers without requesting audio.',
},
volumePct: {
type: 'number',
minimum: 0,
maximum: 100,
description:
'Required for volume; sets the persistent Radio playback volume.',
},
volumePct: { type: 'number', minimum: 0, maximum: 100, description: 'Required for volume; sets the persistent Radio playback volume.' },
category: {
type: 'string',
enum: ['all', 'news', 'talk', 'weather', 'public-safety', 'aviation-marine', 'traffic-transit', 'music'],
description: 'Station category for select/next/previous. When the user requests playback with a category, action must be select, not play.',
enum: [
'all',
'news',
'talk',
'weather',
'public-safety',
'aviation-marine',
'traffic-transit',
'music',
],
description:
'Station category for select/next/previous. When the user requests playback with a category, action must be select, not play.',
},
locationId: {
type: 'string',
enum: ['austin', 'sf', 'nyc', 'tokyo', 'london', 'paris', 'dubai', 'dc'],
enum: [
'austin',
'sf',
'nyc',
'tokyo',
'london',
'paris',
'dubai',
'dc',
],
description: 'Known nearby-city anchor for select.',
},
locationQuery: { type: 'string', maxLength: 120, description: 'Place to search near, such as "Austin, Texas" or "Seattle". Selection does not fly the camera.' },
locationQuery: {
type: 'string',
maxLength: 120,
description:
'Place to search near, such as "Austin, Texas" or "Seattle". Selection does not fly the camera.',
},
latitude: { type: 'number', minimum: -90, maximum: 90 },
longitude: { type: 'number', minimum: -180, maximum: 180 },
country: { type: 'string', maxLength: 80, description: 'Country code or name filter, for example US or United States.' },
stationQuery: { type: 'string', maxLength: 120, description: 'Optional station name/tag substring.' },
country: {
type: 'string',
maxLength: 80,
description:
'Country code or name filter, for example US or United States.',
},
stationQuery: {
type: 'string',
maxLength: 120,
description: 'Optional station name/tag substring.',
},
},
required: ['action'],
},
@@ -415,13 +593,22 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'track_entity',
description: 'Find and follow a specific aircraft (callsign/ICAO hex), ship (name/MMSI), or satellite (name/NORAD id) on enabled layers. Camera follows the entity.',
description:
'Find and follow a specific aircraft (callsign/ICAO hex), ship (name/MMSI), or satellite (name/NORAD id) on enabled layers. Camera follows the entity.',
parameters: {
type: 'object',
additionalProperties: false,
properties: {
query: { type: 'string', description: 'Callsign, ship name, satellite name, ICAO hex, MMSI, or NORAD id.' },
layerId: { type: 'string', description: 'Optional layer hint: flights | military | ais-live-vessels | satellites.' },
query: {
type: 'string',
description:
'Callsign, ship name, satellite name, ICAO hex, MMSI, or NORAD id.',
},
layerId: {
type: 'string',
description:
'Optional layer hint: flights | military | ais-live-vessels | satellites.',
},
},
required: ['query'],
},
@@ -429,7 +616,8 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'stop_tracking',
description: 'Stop following the tracked aircraft/satellite and clear any selected vessel.',
description:
'Stop following the tracked aircraft/satellite and clear any selected vessel.',
parameters: {
type: 'object',
additionalProperties: false,
@@ -439,13 +627,21 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'frame_overhead',
description: 'Cinematically frame entities near the current view: pulls the camera back and angles it so nearby aircraft, ships, or satellites are visible together.',
description:
'Cinematically frame entities near the current view: pulls the camera back and angles it so nearby aircraft, ships, or satellites are visible together.',
parameters: {
type: 'object',
additionalProperties: false,
properties: {
target: { type: 'string', enum: ['flights', 'military', 'satellites', 'vessels'] },
radiusKm: { type: 'number', description: 'Search radius around the view target. Defaults: 150 aircraft, 120 ships, 3000 satellites.' },
target: {
type: 'string',
enum: ['flights', 'military', 'satellites', 'vessels'],
},
radiusKm: {
type: 'number',
description:
'Search radius around the view target. Defaults: 150 aircraft, 120 ships, 3000 satellites.',
},
},
required: ['target'],
},
@@ -453,14 +649,16 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'annotate_map',
description: "Draw annotations on the 3D map to visually point out what you are talking about — like sketching on a whiteboard over the world. Use this whenever you mention a specific place, building, campus, boundary, district, or a relationship between two places, so the user can SEE what you mean. Give place NAMES (preferred) or explicit lat/lng; the app resolves them to real-world positions and real building/area outlines — never guess pixel positions. Call this as you begin describing something, and you may mark several places in one call.",
description:
'Draw annotations on the 3D map to visually point out what you are talking about — like sketching on a whiteboard over the world. Use this whenever you mention a specific place, building, campus, boundary, district, or a relationship between two places, so the user can SEE what you mean. Give place NAMES (preferred) or explicit lat/lng; the app resolves them to real-world positions and real building/area outlines — never guess pixel positions. Call this as you begin describing something, and you may mark several places in one call.',
parameters: {
type: 'object',
additionalProperties: false,
properties: {
annotations: {
type: 'array',
description: 'One or more things to mark. Mark multiple related places together when describing them as a group.',
description:
'One or more things to mark. Mark multiple related places together when describing them as a group.',
minItems: 1,
maxItems: 24,
items: {
@@ -470,19 +668,30 @@ const GEV_REALTIME_TOOLS = [
type: {
type: 'string',
enum: ['pin', 'highlight', 'area', 'arrow', 'route', 'label'],
description: 'pin = planted marker at a spot; highlight = pulsing ring drawing the eye to a point; area = trace the outline of a building/campus/compound/district; arrow = a connector from one place to another (use target as the origin and toTarget as the destination); route = a path through several waypoints (use the points array); label = a floating text callout.',
description:
'pin = planted marker at a spot; highlight = pulsing ring drawing the eye to a point; area = trace the outline of a building/campus/compound/district; arrow = a connector from one place to another (use target as the origin and toTarget as the destination); route = a path through several waypoints (use the points array); label = a floating text callout.',
},
target: {
type: 'string',
maxLength: 200,
description:
'Place name to resolve, e.g. "Palace of Fine Arts, San Francisco", "the Pentagon", "Presidio of San Francisco". Preferred over coordinates. For a specific monument/statue/feature that sits within a larger landmark, use its OWN name + city ("Tejano Monument, Austin", "Texas African American History Memorial, Austin") — do NOT phrase it as "X at the Texas State Capitol", which makes the geocoder collapse several of them onto the same centroid so they stack on one spot.',
},
target: { type: 'string', maxLength: 200, description: 'Place name to resolve, e.g. "Palace of Fine Arts, San Francisco", "the Pentagon", "Presidio of San Francisco". Preferred over coordinates. For a specific monument/statue/feature that sits within a larger landmark, use its OWN name + city ("Tejano Monument, Austin", "Texas African American History Memorial, Austin") — do NOT phrase it as "X at the Texas State Capitol", which makes the geocoder collapse several of them onto the same centroid so they stack on one spot.' },
points: {
type: 'array',
description: 'For type=route: 2+ ordered waypoints the path passes through, each a place name (or coordinates / screen point).',
description:
'For type=route: 2+ ordered waypoints the path passes through, each a place name (or coordinates / screen point).',
minItems: 2,
maxItems: 12,
items: {
type: 'object',
additionalProperties: false,
properties: {
target: { type: 'string', maxLength: 200, description: 'Waypoint place name.' },
target: {
type: 'string',
maxLength: 200,
description: 'Waypoint place name.',
},
latitude: { type: 'number', minimum: -90, maximum: 90 },
longitude: { type: 'number', minimum: -180, maximum: 180 },
screenX: { type: 'number', minimum: 0, maximum: 1 },
@@ -493,32 +702,101 @@ const GEV_REALTIME_TOOLS = [
mode: {
type: 'string',
enum: ['walking', 'driving', 'cycling'],
description: 'For type=route: travel mode for a real street-following route (the app returns distance + time). Pick from the verb the user used ("walk" → walking, "drive" → driving). Defaults to walking.',
description:
'For type=route: travel mode for a real street-following route (the app returns distance + time). Pick from the verb the user used ("walk" → walking, "drive" → driving). Defaults to walking.',
},
latitude: {
type: 'number',
minimum: -90,
maximum: 90,
description:
'Explicit latitude (use only if no good place name exists).',
},
latitude: { type: 'number', minimum: -90, maximum: 90, description: 'Explicit latitude (use only if no good place name exists).' },
longitude: { type: 'number', minimum: -180, maximum: 180 },
toTarget: { type: 'string', maxLength: 200, description: 'For type=arrow: the destination place name.' },
toTarget: {
type: 'string',
maxLength: 200,
description: 'For type=arrow: the destination place name.',
},
toLatitude: { type: 'number', minimum: -90, maximum: 90 },
toLongitude: { type: 'number', minimum: -180, maximum: 180 },
label: { type: 'string', maxLength: 120, description: 'Short caption shown on the map (a few words). Optional.' },
label: {
type: 'string',
maxLength: 120,
description:
'Short caption shown on the map (a few words). Optional.',
},
color: {
type: 'string',
enum: ['primary', 'amber', 'cyan', 'green', 'red'],
description: 'Accent color. primary = neutral, amber = point of interest, cyan = infrastructure, green = confirmed/safe, red = alert.',
description:
'Accent color. primary = neutral, amber = point of interest, cyan = infrastructure, green = confirmed/safe, red = alert.',
},
footprint: {
type: 'boolean',
description:
'For type=area/highlight: trace the real building or campus outline from map data. Defaults true for area.',
},
intent: {
type: 'string',
enum: ['the_thing', 'around_the_thing'],
description:
'For type=area: "the_thing" (default) outlines the place itself (its footprint/boundary); "around_the_thing" highlights a surrounding zone (a buffered radius around it). Infer from phrasing: "the Capitol"/"show me X" → the_thing; "around/near/by X" or "the area around X" → around_the_thing.',
},
entityKind: {
type: 'string',
enum: [
'building',
'compound',
'district',
'street',
'point_feature',
],
description:
'What KIND of thing the target IS — a fact, not a style choice: building = one structure; compound = campus/grounds/mall/park; district = neighborhood or area of a city; street = a named road/corridor; point_feature = monument/statue/memorial/plaque/fountain or other small point landmark. Set it whenever you know it — it routes the resolver to the right footprint source (point_feature anchors monuments as precise points instead of adopting a nearby building outline).',
},
screenX: {
type: 'number',
minimum: 0,
maximum: 1,
description:
'Fallback only: when you cannot name/geocode the place but can SEE it in the latest viewport screenshot, the normalized horizontal position (0=left, 1=right) of the spot. The app converts it back to a real world point under that pixel.',
},
screenY: {
type: 'number',
minimum: 0,
maximum: 1,
description:
'Fallback only: normalized vertical position (0=top, 1=bottom) of the spot in the latest viewport screenshot.',
},
toScreenX: {
type: 'number',
minimum: 0,
maximum: 1,
description:
'For type=arrow: normalized x of the arrow destination from the screenshot (pixel fallback).',
},
toScreenY: {
type: 'number',
minimum: 0,
maximum: 1,
description:
'For type=arrow: normalized y of the arrow destination from the screenshot (pixel fallback).',
},
footprint: { type: 'boolean', description: 'For type=area/highlight: trace the real building or campus outline from map data. Defaults true for area.' },
intent: { type: 'string', enum: ['the_thing', 'around_the_thing'], description: 'For type=area: "the_thing" (default) outlines the place itself (its footprint/boundary); "around_the_thing" highlights a surrounding zone (a buffered radius around it). Infer from phrasing: "the Capitol"/"show me X" → the_thing; "around/near/by X" or "the area around X" → around_the_thing.' },
entityKind: { type: 'string', enum: ['building', 'compound', 'district', 'street', 'point_feature'], description: 'What KIND of thing the target IS — a fact, not a style choice: building = one structure; compound = campus/grounds/mall/park; district = neighborhood or area of a city; street = a named road/corridor; point_feature = monument/statue/memorial/plaque/fountain or other small point landmark. Set it whenever you know it — it routes the resolver to the right footprint source (point_feature anchors monuments as precise points instead of adopting a nearby building outline).' },
screenX: { type: 'number', minimum: 0, maximum: 1, description: 'Fallback only: when you cannot name/geocode the place but can SEE it in the latest viewport screenshot, the normalized horizontal position (0=left, 1=right) of the spot. The app converts it back to a real world point under that pixel.' },
screenY: { type: 'number', minimum: 0, maximum: 1, description: 'Fallback only: normalized vertical position (0=top, 1=bottom) of the spot in the latest viewport screenshot.' },
toScreenX: { type: 'number', minimum: 0, maximum: 1, description: 'For type=arrow: normalized x of the arrow destination from the screenshot (pixel fallback).' },
toScreenY: { type: 'number', minimum: 0, maximum: 1, description: 'For type=arrow: normalized y of the arrow destination from the screenshot (pixel fallback).' },
},
required: ['type'],
},
},
flyTo: { type: 'boolean', description: 'Also move the camera to frame the first annotation. Default false — leave false if the user is already looking at the spot.' },
persist: { type: 'boolean', description: 'Keep annotations until cleared (true, default) or let them auto-fade after ~20s (false).' },
flyTo: {
type: 'boolean',
description:
'Also move the camera to frame the first annotation. Default false — leave false if the user is already looking at the spot.',
},
persist: {
type: 'boolean',
description:
'Keep annotations until cleared (true, default) or let them auto-fade after ~20s (false).',
},
},
required: ['annotations'],
},
@@ -526,7 +804,8 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'clear_annotations',
description: 'Erase ALL map annotations previously drawn with annotate_map. Call this ONLY when the user EXPLICITLY asks to clear or reset the map. Annotations accumulate and persist across navigation and topic changes by design — never clear on your own initiative.',
description:
'Erase ALL map annotations previously drawn with annotate_map. Call this ONLY when the user EXPLICITLY asks to clear or reset the map. Annotations accumulate and persist across navigation and topic changes by design — never clear on your own initiative.',
parameters: {
type: 'object',
additionalProperties: false,
@@ -536,15 +815,29 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'move_camera',
description: 'Direct the camera like a drone operator: orbit the current view target, pan, tilt, or rotate — one bounded nudge (mode=once) or continuous motion until stopped (mode=continuous). Continuous motion also stops on any manual camera input or when a navigation tool runs. Say the RESULTING state when confirming ("Orbiting slowly").',
description:
'Direct the camera like a drone operator: orbit the current view target, pan, tilt, or rotate — one bounded nudge (mode=once) or continuous motion until stopped (mode=continuous). Continuous motion also stops on any manual camera input or when a navigation tool runs. Say the RESULTING state when confirming ("Orbiting slowly").',
parameters: {
type: 'object',
additionalProperties: false,
properties: {
motion: { type: 'string', enum: ['orbit', 'pan', 'tilt', 'rotate', 'stop'] },
direction: { type: 'string', enum: ['left', 'right', 'up', 'down'], description: 'Required except for orbit (defaults right/clockwise) and stop.' },
motion: {
type: 'string',
enum: ['orbit', 'pan', 'tilt', 'rotate', 'stop'],
},
direction: {
type: 'string',
enum: ['left', 'right', 'up', 'down'],
description:
'Required except for orbit (defaults right/clockwise) and stop.',
},
speed: { type: 'string', enum: ['slow', 'normal', 'fast'] },
mode: { type: 'string', enum: ['once', 'continuous'], description: 'once = bounded eased nudge (default); continuous = until stop/manual input.' },
mode: {
type: 'string',
enum: ['once', 'continuous'],
description:
'once = bounded eased nudge (default); continuous = until stop/manual input.',
},
},
required: ['motion'],
},
@@ -552,12 +845,16 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'fly_route',
description: 'Cinematic dolly along an EXISTING route annotation (drawn earlier with annotate_map type=route) — flies the street-following path from start to end. Omit label for the newest route. If no route is drawn, this fails with guidance: draw the route first.',
description:
'Cinematic dolly along an EXISTING route annotation (drawn earlier with annotate_map type=route) — flies the street-following path from start to end. Omit label for the newest route. If no route is drawn, this fails with guidance: draw the route first.',
parameters: {
type: 'object',
additionalProperties: false,
properties: {
label: { type: 'string', description: 'Match an existing route mark by (partial) label.' },
label: {
type: 'string',
description: 'Match an existing route mark by (partial) label.',
},
speed: { type: 'string', enum: ['slow', 'normal', 'fast'] },
},
},
@@ -565,59 +862,112 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'analyst_query',
description: 'Answer questions ABOUT the data currently loaded on the map — counts, lists, superlatives, and attribute filters over live layers (flights, military, ships, fires, earthquakes). Examples: "how many flights over Texas", "biggest fire near LA", "which ships are headed to Oakland", "anything above 40,000 feet", "fastest thing in view". Queries ONLY client-side data from ENABLED layers — if the needed layer is off, say so and offer to enable it. For a follow-up about the previous answer\'s set ("which of those is closest?"), set followUp=true and send only the new filters/sort.',
description:
'Answer questions ABOUT the data currently loaded on the map — counts, lists, superlatives, and attribute filters over live layers (flights, military, ships, fires, earthquakes). Examples: "how many flights over Texas", "biggest fire near LA", "which ships are headed to Oakland", "anything above 40,000 feet", "fastest thing in view". Queries ONLY client-side data from ENABLED layers — if the needed layer is off, say so and offer to enable it. For a follow-up about the previous answer\'s set ("which of those is closest?"), set followUp=true and send only the new filters/sort.',
parameters: {
type: 'object',
additionalProperties: false,
properties: {
layers: {
type: 'array',
items: { type: 'string', enum: ['flights', 'military', 'ais-live-vessels', 'local-firms', 'earthquakes'] },
description: 'Layers to query. fires/wildfires → local-firms; ships/vessels → ais-live-vessels.',
items: {
type: 'string',
enum: [
'flights',
'military',
'ais-live-vessels',
'local-firms',
'earthquakes',
],
},
description:
'Layers to query. fires/wildfires → local-firms; ships/vessels → ais-live-vessels.',
},
scope: {
type: 'object',
additionalProperties: false,
description: 'Spatial scope. Default: view (near the camera). Use kind=region for "over Texas"-style asks; kind=anywhere for global questions.',
description:
'Spatial scope. Default: view (near the camera). Use kind=region for "over Texas"-style asks; kind=anywhere for global questions.',
properties: {
kind: { type: 'string', enum: ['view', 'region', 'radius', 'anywhere'] },
name: { type: 'string', description: 'For kind=region: a state/country ("Texas", "France") or a named natural region ("the Alps", "Gulf of Mexico").' },
kind: {
type: 'string',
enum: ['view', 'region', 'radius', 'anywhere'],
},
name: {
type: 'string',
description:
'For kind=region: a state/country ("Texas", "France") or a named natural region ("the Alps", "Gulf of Mexico").',
},
km: { type: 'number', description: 'For kind=radius.' },
center: { type: 'object', additionalProperties: false, properties: { lat: { type: 'number' }, lon: { type: 'number' } } },
center: {
type: 'object',
additionalProperties: false,
properties: { lat: { type: 'number' }, lon: { type: 'number' } },
},
},
},
filters: {
type: 'array',
description: 'Attribute predicates, ANDed. ALTITUDE IS METERS (40,000 ft = 12192). Fields: altitudeM, speedMps, military, onGround, aircraftClass, callsign, operator, routeOrigin, routeDestination, originCountry (flights); speedKts, shipType, destination (ships); frp, confidence (fires); magnitude, depthKm, place (earthquakes).',
description:
'Attribute predicates, ANDed. ALTITUDE IS METERS (40,000 ft = 12192). Fields: altitudeM, speedMps, military, onGround, aircraftClass, callsign, operator, routeOrigin, routeDestination, originCountry (flights); speedKts, shipType, destination (ships); frp, confidence (fires); magnitude, depthKm, place (earthquakes).',
items: {
type: 'object',
additionalProperties: false,
properties: {
field: { type: 'string' },
op: { type: 'string', enum: ['gt', 'gte', 'lt', 'lte', 'eq', 'neq', 'contains'] },
op: {
type: 'string',
enum: ['gt', 'gte', 'lt', 'lte', 'eq', 'neq', 'contains'],
},
value: {},
},
required: ['field', 'op', 'value'],
},
},
sortBy: { type: 'string', description: 'Field to rank by, or "distance" for nearest-first.' },
sortBy: {
type: 'string',
description: 'Field to rank by, or "distance" for nearest-first.',
},
sortDir: { type: 'string', enum: ['asc', 'desc'] },
limit: { type: 'number' },
followUp: { type: 'boolean', description: 'true = re-query the PREVIOUS result set instead of fresh data.' },
followUp: {
type: 'boolean',
description:
'true = re-query the PREVIOUS result set instead of fresh data.',
},
},
},
},
{
type: 'function',
name: 'next_iss_pass',
description: "When the user asks when the ISS / the space station will next fly over: returns the next visible ISS pass for the current camera location (or an explicit lat/lon) — rise time (ISO + minutes from now), rise compass direction, peak elevation, and duration. Requires the satellites layer to have loaded its catalog at least once this session; if it hasn't, tell the user to enable the satellites layer and try again.",
description:
"When the user asks when the ISS / the space station will next fly over: returns the next visible ISS pass for the current camera location (or an explicit lat/lon) — rise time (ISO + minutes from now), rise compass direction, peak elevation, and duration. Requires the satellites layer to have loaded its catalog at least once this session; if it hasn't, tell the user to enable the satellites layer and try again.",
parameters: {
type: 'object',
additionalProperties: false,
properties: {
latitude: { type: 'number', minimum: -90, maximum: 90, description: 'Optional observer latitude. Omit to use the current camera position.' },
longitude: { type: 'number', minimum: -180, maximum: 180, description: 'Optional observer longitude. Omit to use the current camera position.' },
minElevationDeg: { type: 'number', minimum: 5, maximum: 60, description: 'Minimum peak elevation (deg) to count as a pass. Default 10.' },
latitude: {
type: 'number',
minimum: -90,
maximum: 90,
description:
'Optional observer latitude. Omit to use the current camera position.',
},
longitude: {
type: 'number',
minimum: -180,
maximum: 180,
description:
'Optional observer longitude. Omit to use the current camera position.',
},
minElevationDeg: {
type: 'number',
minimum: 5,
maximum: 60,
description:
'Minimum peak elevation (deg) to count as a pass. Default 10.',
},
},
},
},
+41 -12
View File
@@ -1,9 +1,23 @@
import { makeRateLimiter, clientKey } from './common/rate-limit.js';
import { readRequestBodyCapped } from './common/request.js';
import { OVERPASS_MAX_BODY_BYTES, OVERPASS_MAX_CONCURRENT } from './overpass/constants.js';
import {
OVERPASS_MAX_BODY_BYTES,
OVERPASS_MAX_CONCURRENT,
} from './overpass/constants.js';
import { sanitizeOverpassBody } from './overpass/query.js';
import { resolveOverpassPreflight, _overpassCache, readOverpassDisk, overpassDiskTtlMs, readStaleOverpass, trimOverpassCache, writeOverpassDisk } from './overpass/cache.js';
import { overpassPayloadIsData, fetchOverpassPayload } from './overpass/transport.js';
import {
resolveOverpassPreflight,
_overpassCache,
readOverpassDisk,
overpassDiskTtlMs,
readStaleOverpass,
trimOverpassCache,
writeOverpassDisk,
} from './overpass/cache.js';
import {
overpassPayloadIsData,
fetchOverpassPayload,
} from './overpass/transport.js';
import { installRouteMiddleware } from './places/routes.js';
/** @type {Map<string,Promise>} In-flight Overpass requests keyed by normalized query body. */
@@ -11,7 +25,11 @@ const _overpassInFlight = new Map();
let _overpassConcurrent = 0;
const _overpassRateLimiter = makeRateLimiter({ windowMs: 60_000, max: 90, globalMax: 300 });
const _overpassRateLimiter = makeRateLimiter({
windowMs: 60_000,
max: 90,
globalMax: 300,
});
/**
* Write a completed Overpass payload to the HTTP response.
@@ -57,7 +75,9 @@ function overpassProxy() {
// Collect POST body with a hard byte cap (Overpass QL queries are small)
let body;
try {
body = (await readRequestBodyCapped(req, OVERPASS_MAX_BODY_BYTES)).toString();
body = (
await readRequestBodyCapped(req, OVERPASS_MAX_BODY_BYTES)
).toString();
} catch (err) {
if (err?.code === 'BODY_TOO_LARGE') {
res.writeHead(413, { 'Content-Type': 'application/json' });
@@ -90,11 +110,15 @@ function overpassProxy() {
inFlight: _overpassInFlight,
// Fresh-enough disk entries survive restarts and skip the public
// mirrors; boundary-class queries keep their month-long TTL.
readDisk: () => readOverpassDisk(cacheKey, overpassDiskTtlMs(cacheKey)),
readDisk: () =>
readOverpassDisk(cacheKey, overpassDiskTtlMs(cacheKey)),
allowUpstream: () => _overpassRateLimiter(clientKey(req)),
});
if (preflight.source === 'RATE_LIMITED') {
res.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': '5' });
res.writeHead(429, {
'Content-Type': 'application/json',
'Retry-After': '5',
});
res.end(JSON.stringify({ error: 'Rate limit exceeded' }));
return;
}
@@ -119,8 +143,15 @@ function overpassProxy() {
// From here onward the request is genuinely upstream-bound and has
// consumed one local limiter slot. Cache and dedupe hits above do not.
if (_overpassConcurrent >= OVERPASS_MAX_CONCURRENT) {
res.writeHead(503, { 'Content-Type': 'application/json', 'Retry-After': '2' });
res.end(JSON.stringify({ error: 'Overpass proxy busy — try again shortly' }));
res.writeHead(503, {
'Content-Type': 'application/json',
'Retry-After': '2',
});
res.end(
JSON.stringify({
error: 'Overpass proxy busy — try again shortly',
}),
);
return;
}
_overpassConcurrent += 1;
@@ -158,9 +189,7 @@ function overpassProxy() {
sendOverpassResponse(res, payload, 'MISS');
} catch (e) {
// Every mirror threw (network-level). Same serve-stale rule.
const stale = cacheKey
? await readStaleOverpass(cacheKey)
: null;
const stale = cacheKey ? await readStaleOverpass(cacheKey) : null;
if (stale) {
sendOverpassResponse(res, stale, 'STALE');
return;
+45 -10
View File
@@ -1,5 +1,11 @@
import { isOverpassBoundaryQuery } from './query.js';
import { OVERPASS_BOUNDARY_DISK_TTL_MS, OVERPASS_DISK_TTL_MS, OVERPASS_DISK_DIR, OVERPASS_CACHE_MS, OVERPASS_CACHE_MAX_ENTRIES } from './constants.js';
import {
OVERPASS_BOUNDARY_DISK_TTL_MS,
OVERPASS_DISK_TTL_MS,
OVERPASS_DISK_DIR,
OVERPASS_CACHE_MS,
OVERPASS_CACHE_MAX_ENTRIES,
} from './constants.js';
import path from 'node:path';
import { createHash } from 'node:crypto';
import { promises as fsp } from 'node:fs';
@@ -10,12 +16,17 @@ const _overpassCache = new Map();
/** Disk TTL for a query: boundary geometry keeps for a month, the rest 7 days. */
function overpassDiskTtlMs(cacheKey) {
return isOverpassBoundaryQuery(cacheKey) ? OVERPASS_BOUNDARY_DISK_TTL_MS : OVERPASS_DISK_TTL_MS;
return isOverpassBoundaryQuery(cacheKey)
? OVERPASS_BOUNDARY_DISK_TTL_MS
: OVERPASS_DISK_TTL_MS;
}
/** Normalized Overpass query -> stable disk-cache file path. */
function overpassDiskPath(cacheKey) {
return path.join(OVERPASS_DISK_DIR, `${createHash('sha1').update(cacheKey).digest('hex')}.json`);
return path.join(
OVERPASS_DISK_DIR,
`${createHash('sha1').update(cacheKey).digest('hex')}.json`,
);
}
/**
@@ -27,7 +38,12 @@ async function readOverpassDisk(cacheKey, maxAgeMs) {
try {
const raw = await fsp.readFile(overpassDiskPath(cacheKey), 'utf8');
const payload = JSON.parse(raw);
if (!payload || typeof payload.body !== 'string' || !Number.isFinite(payload.cachedAt)) return null;
if (
!payload ||
typeof payload.body !== 'string' ||
!Number.isFinite(payload.cachedAt)
)
return null;
// Older versions persisted 4xx refusals with normal data TTLs. Ignore
// them on both fresh and stale reads so an upgrade can recover immediately.
if (!overpassPayloadIsData(payload)) return null;
@@ -40,9 +56,17 @@ async function readOverpassDisk(cacheKey, maxAgeMs) {
/** Fire-and-forget disk write for a successful Overpass payload. */
function writeOverpassDisk(cacheKey, payload) {
fsp.mkdir(OVERPASS_DISK_DIR, { recursive: true })
.then(() => fsp.writeFile(overpassDiskPath(cacheKey), JSON.stringify(payload)))
.catch((err) => console.warn('[Overpass Proxy] disk cache write failed:', err?.message || err));
fsp
.mkdir(OVERPASS_DISK_DIR, { recursive: true })
.then(() =>
fsp.writeFile(overpassDiskPath(cacheKey), JSON.stringify(payload)),
)
.catch((err) =>
console.warn(
'[Overpass Proxy] disk cache write failed:',
err?.message || err,
),
);
}
/**
@@ -71,7 +95,8 @@ async function resolveOverpassPreflight({
cacheMs = OVERPASS_CACHE_MS,
}) {
const cached = memoryCache.get(cacheKey);
if (overpassPayloadIsData(cached) && now - cached.cachedAt <= cacheMs) return { source: 'HIT', payload: cached };
if (overpassPayloadIsData(cached) && now - cached.cachedAt <= cacheMs)
return { source: 'HIT', payload: cached };
const pending = inFlight.get(cacheKey);
if (pending) return { source: 'INFLIGHT', payload: await pending };
@@ -87,7 +112,9 @@ async function resolveOverpassPreflight({
/** Return only last-good Overpass data, regardless of its age. */
async function readStaleOverpass(cacheKey) {
const cached = _overpassCache.get(cacheKey);
return overpassPayloadIsData(cached) ? cached : readOverpassDisk(cacheKey, Infinity);
return overpassPayloadIsData(cached)
? cached
: readOverpassDisk(cacheKey, Infinity);
}
/** Evict oldest Overpass cache entries until size is within the cap. */
@@ -99,4 +126,12 @@ function trimOverpassCache() {
}
}
export { readOverpassDisk, resolveOverpassPreflight, _overpassCache, overpassDiskTtlMs, readStaleOverpass, trimOverpassCache, writeOverpassDisk };
export {
readOverpassDisk,
resolveOverpassPreflight,
_overpassCache,
overpassDiskTtlMs,
readStaleOverpass,
trimOverpassCache,
writeOverpassDisk,
};
+30 -4
View File
@@ -97,12 +97,38 @@ const OVERPASS_MAX_BBOX_DEG = 12;
const OVERPASS_ELEMENT_TYPES = 'node|way|relation|nwr|nw|nr|wr|rel';
/** Element-selector (incl. `area`) whose statements must be individually bounded. */
const OVERPASS_SELECTOR_RE = new RegExp(`\\b(?:${OVERPASS_ELEMENT_TYPES}|area)\\b`);
const OVERPASS_SELECTOR_RE = new RegExp(
`\\b(?:${OVERPASS_ELEMENT_TYPES}|area)\\b`,
);
/** An element selector bounded BY an area — the country-scan abuse shape. */
const OVERPASS_AREA_ELEMENT_RE = new RegExp(`\\b(?:${OVERPASS_ELEMENT_TYPES})\\s*\\(\\s*area\\b`, 'i');
const OVERPASS_AREA_ELEMENT_RE = new RegExp(
`\\b(?:${OVERPASS_ELEMENT_TYPES})\\s*\\(\\s*area\\b`,
'i',
);
/** A single bbox 4-tuple `(s,w,n,e)` (non-global so it does not advance lastIndex). */
const OVERPASS_BBOX_RE = /\(\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*\)/;
const OVERPASS_BBOX_RE =
/\(\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*\)/;
export { OVERPASS_BOUNDARY_DISK_TTL_MS, OVERPASS_DISK_TTL_MS, OVERPASS_DISK_DIR, OVERPASS_CACHE_MS, OVERPASS_CACHE_MAX_ENTRIES, OVERPASS_MAX_BODY_BYTES, OVERPASS_MAX_CONCURRENT, OVERPASS_MAX_AROUND_M, OVERPASS_MAX_BBOX_DEG, OVERPASS_AREA_ELEMENT_RE, OVERPASS_SELECTOR_RE, OVERPASS_BBOX_RE, OVERPASS_MAX_QL_TIMEOUT, OVERPASS_SIMPLIFY_MIN_BYTES, OVERPASS_SIMPLIFY_MIN_POINTS, OVERPASS_SIMPLIFY_TOLERANCE_DEG, OVERPASS_MAX_RESPONSE_BYTES, OVERPASS_UPSTREAMS, OVERPASS_TIMEOUT_MS };
export {
OVERPASS_BOUNDARY_DISK_TTL_MS,
OVERPASS_DISK_TTL_MS,
OVERPASS_DISK_DIR,
OVERPASS_CACHE_MS,
OVERPASS_CACHE_MAX_ENTRIES,
OVERPASS_MAX_BODY_BYTES,
OVERPASS_MAX_CONCURRENT,
OVERPASS_MAX_AROUND_M,
OVERPASS_MAX_BBOX_DEG,
OVERPASS_AREA_ELEMENT_RE,
OVERPASS_SELECTOR_RE,
OVERPASS_BBOX_RE,
OVERPASS_MAX_QL_TIMEOUT,
OVERPASS_SIMPLIFY_MIN_BYTES,
OVERPASS_SIMPLIFY_MIN_POINTS,
OVERPASS_SIMPLIFY_TOLERANCE_DEG,
OVERPASS_MAX_RESPONSE_BYTES,
OVERPASS_UPSTREAMS,
OVERPASS_TIMEOUT_MS,
};
+13 -4
View File
@@ -1,4 +1,8 @@
import { OVERPASS_SIMPLIFY_MIN_BYTES, OVERPASS_SIMPLIFY_MIN_POINTS, OVERPASS_SIMPLIFY_TOLERANCE_DEG } from './constants.js';
import {
OVERPASS_SIMPLIFY_MIN_BYTES,
OVERPASS_SIMPLIFY_MIN_POINTS,
OVERPASS_SIMPLIFY_TOLERANCE_DEG,
} from './constants.js';
/** Iterative Douglas-Peucker on [{lat,lon},...] (planar-degree approx — fine at
* the ~44 m tolerance used here). Endpoints always kept. */
@@ -51,7 +55,10 @@ function simplifyElementGeometry(el, minPoints, toleranceDeg) {
}
if (Array.isArray(el?.members)) {
for (const member of el.members) {
if (Array.isArray(member?.geometry) && member.geometry.length >= minPoints) {
if (
Array.isArray(member?.geometry) &&
member.geometry.length >= minPoints
) {
member.geometry = douglasPeucker(member.geometry, toleranceDeg);
}
}
@@ -73,7 +80,8 @@ function simplifyOverpassPayloadBody(bodyText, opts = {}) {
const minBytes = opts.minBytes ?? OVERPASS_SIMPLIFY_MIN_BYTES;
const minPoints = opts.minPoints ?? OVERPASS_SIMPLIFY_MIN_POINTS;
const toleranceDeg = opts.toleranceDeg ?? OVERPASS_SIMPLIFY_TOLERANCE_DEG;
if (typeof bodyText !== 'string' || bodyText.length < minBytes) return bodyText;
if (typeof bodyText !== 'string' || bodyText.length < minBytes)
return bodyText;
let data;
try {
data = JSON.parse(bodyText);
@@ -81,7 +89,8 @@ function simplifyOverpassPayloadBody(bodyText, opts = {}) {
return bodyText;
}
if (!Array.isArray(data?.elements)) return bodyText;
for (const el of data.elements) simplifyElementGeometry(el, minPoints, toleranceDeg);
for (const el of data.elements)
simplifyElementGeometry(el, minPoints, toleranceDeg);
try {
return JSON.stringify(data);
} catch {
+54 -17
View File
@@ -1,4 +1,11 @@
import { OVERPASS_MAX_AROUND_M, OVERPASS_MAX_BBOX_DEG, OVERPASS_AREA_ELEMENT_RE, OVERPASS_SELECTOR_RE, OVERPASS_BBOX_RE, OVERPASS_MAX_QL_TIMEOUT } from './constants.js';
import {
OVERPASS_MAX_AROUND_M,
OVERPASS_MAX_BBOX_DEG,
OVERPASS_AREA_ELEMENT_RE,
OVERPASS_SELECTOR_RE,
OVERPASS_BBOX_RE,
OVERPASS_MAX_QL_TIMEOUT,
} from './constants.js';
/**
* Whether a normalized Overpass query is BOUNDARY-class (admin `is_in` lookups
@@ -41,8 +48,14 @@ function stripOverpassNoise(src) {
const quote = c;
i += 1;
while (i < n) {
if (src[i] === '\\') { i += 2; continue; } // escaped char
if (src[i] === quote) { i += 1; break; } // closing quote
if (src[i] === '\\') {
i += 2;
continue;
} // escaped char
if (src[i] === quote) {
i += 1;
break;
} // closing quote
i += 1;
}
out += quote + quote; // collapse the literal to empty quotes
@@ -69,11 +82,17 @@ function stripOverpassNoise(src) {
function sanitizeOverpassBody(rawBody) {
let params;
try { params = new URLSearchParams(rawBody); } catch { return { ok: false, error: 'Malformed query body' }; }
try {
params = new URLSearchParams(rawBody);
} catch {
return { ok: false, error: 'Malformed query body' };
}
const all = params.getAll('data');
if (all.length !== 1) return { ok: false, error: 'Exactly one data query is required' };
if (all.length !== 1)
return { ok: false, error: 'Exactly one data query is required' };
const data = all[0];
if (!data || !data.trim()) return { ok: false, error: 'Missing Overpass data query' };
if (!data || !data.trim())
return { ok: false, error: 'Missing Overpass data query' };
// Blank quoted literals + strip comments in one lexer pass so a fake bound or a
// `//` inside a string can't hide an unbounded selector (or satisfy a bound).
@@ -89,9 +108,17 @@ function sanitizeOverpassBody(rawBody) {
}
}
// Reject world-sized / oversized bboxes.
for (const m of stripped.matchAll(/\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/g)) {
const s = Number(m[1]); const w = Number(m[2]); const n = Number(m[3]); const e = Number(m[4]);
if (Math.abs(n - s) > OVERPASS_MAX_BBOX_DEG || Math.abs(e - w) > OVERPASS_MAX_BBOX_DEG) {
for (const m of stripped.matchAll(
/\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/g,
)) {
const s = Number(m[1]);
const w = Number(m[2]);
const n = Number(m[3]);
const e = Number(m[4]);
if (
Math.abs(n - s) > OVERPASS_MAX_BBOX_DEG ||
Math.abs(e - w) > OVERPASS_MAX_BBOX_DEG
) {
return { ok: false, error: 'Overpass bbox too large' };
}
}
@@ -122,7 +149,10 @@ function sanitizeOverpassBody(rawBody) {
// misread as a spatial bound. Bounds live in (...) / function calls / set
// refs, never inside [...], so the probe loses nothing real.
const outSets = [];
const body = stmt.replace(/->\s*\.(\w+)/g, (_, name) => { outSets.push(name); return ' '; });
const body = stmt.replace(/->\s*\.(\w+)/g, (_, name) => {
outSets.push(name);
return ' ';
});
const probe = body.replace(/\[[^\]]*\]/g, ' ');
// Reject element-in-area scans on the TAG-STRIPPED probe, so a tag filter
@@ -131,15 +161,21 @@ function sanitizeOverpassBody(rawBody) {
// SELECTS admin areas (area.set) and pivots (rel(pivot.x)), never node/way/
// relation(area...). The probe collapses tags so `way (area.a)` is caught.
if (OVERPASS_AREA_ELEMENT_RE.test(probe)) {
return { ok: false, error: 'Overpass area-bounded element selector not allowed' };
return {
ok: false,
error: 'Overpass area-bounded element selector not allowed',
};
}
const hasSelector = OVERPASS_SELECTOR_RE.test(probe);
const inputSets = [...probe.matchAll(/(?<!\d)\.([a-z_]\w*)/gi)].map((m) => m[1]);
const directBound = /around:\s*\d/.test(probe)
|| OVERPASS_BBOX_RE.test(probe)
|| /is_in\s*\(/.test(probe) // is_in(lat,lon) — the function form only
|| /\barea\s*\(/.test(probe); // area(id) — bounded as a set definition
const inputSets = [...probe.matchAll(/(?<!\d)\.([a-z_]\w*)/gi)].map(
(m) => m[1],
);
const directBound =
/around:\s*\d/.test(probe) ||
OVERPASS_BBOX_RE.test(probe) ||
/is_in\s*\(/.test(probe) || // is_in(lat,lon) — the function form only
/\barea\s*\(/.test(probe); // area(id) — bounded as a set definition
const setBound = inputSets.some((s) => boundedSets.has(s));
const bounded = directBound || setBound;
@@ -153,7 +189,8 @@ function sanitizeOverpassBody(rawBody) {
const clamped = data.replace(
/\[timeout:\s*(\d+)\s*\]/gi,
(_, n) => `[timeout:${Math.min(Number(n) || OVERPASS_MAX_QL_TIMEOUT, OVERPASS_MAX_QL_TIMEOUT)}]`,
(_, n) =>
`[timeout:${Math.min(Number(n) || OVERPASS_MAX_QL_TIMEOUT, OVERPASS_MAX_QL_TIMEOUT)}]`,
);
return { ok: true, body: `data=${encodeURIComponent(clamped)}` };
}
+36 -17
View File
@@ -1,4 +1,8 @@
import { OVERPASS_MAX_RESPONSE_BYTES, OVERPASS_UPSTREAMS, OVERPASS_TIMEOUT_MS } from './constants.js';
import {
OVERPASS_MAX_RESPONSE_BYTES,
OVERPASS_UPSTREAMS,
OVERPASS_TIMEOUT_MS,
} from './constants.js';
import { readResponseTextCapped } from '../common/http.js';
import { simplifyOverpassPayloadBody } from './geometry.js';
@@ -13,10 +17,12 @@ import { simplifyOverpassPayloadBody } from './geometry.js';
*/
function overpassLooksRateLimited(bodyText) {
const text = String(bodyText || '').toLowerCase();
return text.includes('rate_limited')
|| text.includes('quota of your ip address')
|| text.includes('dispatcher_client::request_read_and_idx::rate_limited')
|| text.includes('too many requests');
return (
text.includes('rate_limited') ||
text.includes('quota of your ip address') ||
text.includes('dispatcher_client::request_read_and_idx::rate_limited') ||
text.includes('too many requests')
);
}
/**
@@ -26,9 +32,11 @@ function overpassLooksRateLimited(bodyText) {
*/
function overpassLooksRuntimeError(bodyText) {
const text = String(bodyText || '').toLowerCase();
return text.includes('runtime error')
|| text.includes('timed out')
|| text.includes('out of memory');
return (
text.includes('runtime error') ||
text.includes('timed out') ||
text.includes('out of memory')
);
}
/**
@@ -42,10 +50,13 @@ function overpassLooksRuntimeError(bodyText) {
*/
function overpassPayloadIsData(payload) {
const status = Number(payload?.status);
return Number.isFinite(status)
&& status >= 200 && status < 300
&& !payload.rateLimited
&& !payload.runtimeError;
return (
Number.isFinite(status) &&
status >= 200 &&
status < 300 &&
!payload.rateLimited &&
!payload.runtimeError
);
}
/**
@@ -57,12 +68,16 @@ function overpassPayloadIsData(payload) {
* @param {object} [options] Server-only endpoint and I/O overrides for tests.
* @returns {Promise<{status:number,body:string,contentType:string,endpoint:string,rateLimited:boolean}>}
*/
async function fetchOverpassPayload(body, maxResponseBytes = OVERPASS_MAX_RESPONSE_BYTES, {
async function fetchOverpassPayload(
body,
maxResponseBytes = OVERPASS_MAX_RESPONSE_BYTES,
{
endpoints = OVERPASS_UPSTREAMS,
fetchImpl = fetch,
readBody = readResponseTextCapped,
simplify = simplifyOverpassPayloadBody,
} = {}) {
} = {},
) {
let lastError = null;
let lastRateLimitPayload = null;
let lastRefusalPayload = null;
@@ -83,9 +98,11 @@ async function fetchOverpassPayload(body, maxResponseBytes = OVERPASS_MAX_RESPON
});
const responseBody = await readBody(upstream, maxResponseBytes);
const contentType = upstream.headers.get('content-type') || 'application/json';
const contentType =
upstream.headers.get('content-type') || 'application/json';
const status = upstream.status;
const rateLimited = status === 429 || overpassLooksRateLimited(responseBody);
const rateLimited =
status === 429 || overpassLooksRateLimited(responseBody);
const runtimeError = overpassLooksRuntimeError(responseBody);
const payload = {
status,
@@ -116,7 +133,9 @@ async function fetchOverpassPayload(body, maxResponseBytes = OVERPASS_MAX_RESPON
// every mirror has had the chance to answer it.
if (status < 200 || status >= 300) {
if (!lastRefusalPayload) lastRefusalPayload = payload;
lastError = new Error(`Overpass upstream returned ${status} (${endpoint})`);
lastError = new Error(
`Overpass upstream returned ${status} (${endpoint})`,
);
continue;
}
+4 -1
View File
@@ -1,3 +1,6 @@
export { regionalBriefProxy, regionalBriefHasAnySource } from './regional/briefing.js';
export {
regionalBriefProxy,
regionalBriefHasAnySource,
} from './regional/briefing.js';
export { weatherEffectsProxy } from './regional/weather-effects.js';
export { validRegionalPoint } from './regional/query.js';
+40 -10
View File
@@ -18,7 +18,11 @@ const _regionalBriefCache = new Map();
const _regionalBriefInFlight = new Map();
const _regionalBriefRateLimiter = makeRateLimiter({ windowMs: 60_000, max: 30, globalMax: 90 });
const _regionalBriefRateLimiter = makeRateLimiter({
windowMs: 60_000,
max: 30,
globalMax: 90,
});
function trimRegionalBriefCache() {
while (_regionalBriefCache.size > REGIONAL_BRIEF_MAX_CACHE) {
@@ -40,13 +44,15 @@ function regionalBriefProxy() {
fetchRegionalWeather(point),
]);
const place = placeResult.status === 'fulfilled' ? placeResult.value : null;
const weather = weatherResult.status === 'fulfilled' ? weatherResult.value : null;
const weather =
weatherResult.status === 'fulfilled' ? weatherResult.value : null;
const news = await fetchRegionalNews(place);
if (!regionalBriefHasAnySource({ place, weather, news })) {
throw new Error('All regional briefing sources unavailable');
}
const payload = {
status: place && weather && news.status !== 'unavailable' ? 'ready' : 'partial',
status:
place && weather && news.status !== 'unavailable' ? 'ready' : 'partial',
retrievedAt: new Date().toISOString(),
coordinates: point,
place,
@@ -71,7 +77,10 @@ function regionalBriefProxy() {
return;
}
if (!_regionalBriefRateLimiter(clientKey(req))) {
res.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': '10' });
res.writeHead(429, {
'Content-Type': 'application/json',
'Retry-After': '10',
});
res.end(JSON.stringify({ error: 'Rate limit exceeded' }));
return;
}
@@ -79,18 +88,28 @@ function regionalBriefProxy() {
const point = validRegionalPoint(url.searchParams);
if (!point) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Valid latitude and longitude are required' }));
res.end(
JSON.stringify({
error: 'Valid latitude and longitude are required',
}),
);
return;
}
const key = `${(Math.round(point.latitude * 10) / 10).toFixed(1)},${(Math.round(point.longitude * 10) / 10).toFixed(1)}`;
const now = Date.now();
const cached = _regionalBriefCache.get(key);
if (cached && now - cached.cachedAt <= REGIONAL_BRIEF_CACHE_MS) {
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=60', 'X-Regional-Brief': 'HIT' });
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=60',
'X-Regional-Brief': 'HIT',
});
res.end(JSON.stringify({ ...cached.payload, status: 'cached' }));
return;
}
const request = coalesceProxyRequest(_regionalBriefInFlight, key, () => refresh(point, key));
const request = coalesceProxyRequest(_regionalBriefInFlight, key, () =>
refresh(point, key),
);
try {
const payload = await request.promise;
res.writeHead(200, {
@@ -101,12 +120,23 @@ function regionalBriefProxy() {
res.end(JSON.stringify(payload));
} catch {
if (cached && now - cached.cachedAt <= REGIONAL_BRIEF_STALE_MS) {
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', 'X-Regional-Brief': 'STALE' });
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
'X-Regional-Brief': 'STALE',
});
res.end(JSON.stringify({ ...cached.payload, status: 'stale' }));
return;
}
res.writeHead(503, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ error: 'Regional briefing is temporarily unavailable' }));
res.writeHead(503, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
res.end(
JSON.stringify({
error: 'Regional briefing is temporarily unavailable',
}),
);
}
});
}
+14 -5
View File
@@ -1,12 +1,18 @@
import { readResponseJsonCapped, readResponseTextCapped } from '../common/http.js';
import {
readResponseJsonCapped,
readResponseTextCapped,
} from '../common/http.js';
const REGIONAL_MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
async function fetchRegionalJson(url, {
async function fetchRegionalJson(
url,
{
headers = {},
timeoutMs = 9000,
maxBytes = REGIONAL_MAX_RESPONSE_BYTES,
} = {}) {
} = {},
) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
@@ -18,11 +24,14 @@ async function fetchRegionalJson(url, {
}
}
async function fetchRegionalText(url, {
async function fetchRegionalText(
url,
{
headers = {},
timeoutMs = 9000,
maxBytes = REGIONAL_MAX_RESPONSE_BYTES,
} = {}) {
} = {},
) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
+47 -15
View File
@@ -4,24 +4,39 @@ import { normalizeRegionalArticles } from '../../../src/data/regionalBrief.js';
function decodeRssText(value) {
return String(value || '')
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1')
.replace(/&amp;/g, '&').replace(/&quot;/g, '"').replace(/&#39;/g, "'")
.replace(/&lt;/g, '<').replace(/&gt;/g, '>')
.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
.replace(/&amp;/g, '&')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function rssTag(block, tag) {
return decodeRssText(new RegExp(`<${tag}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${tag}>`, 'i').exec(block)?.[1] || '');
return decodeRssText(
new RegExp(`<${tag}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${tag}>`, 'i').exec(
block,
)?.[1] || '',
);
}
function normalizeRssArticles(xml, limit = 5) {
const seen = new Set();
const articles = [];
for (const match of String(xml || '').matchAll(/<item>([\s\S]*?)<\/item>/gi)) {
for (const match of String(xml || '').matchAll(
/<item>([\s\S]*?)<\/item>/gi,
)) {
const item = match[1];
const title = rssTag(item, 'title').slice(0, 180);
const url = rssTag(item, 'link');
let parsedUrl;
try { parsedUrl = new URL(url); } catch { continue; }
try {
parsedUrl = new URL(url);
} catch {
continue;
}
if (!title || !['http:', 'https:'].includes(parsedUrl.protocol)) continue;
const source = rssTag(item, 'source');
const signature = `${title.toLowerCase()}|${source.toLowerCase() || parsedUrl.hostname}`;
@@ -32,7 +47,9 @@ function normalizeRssArticles(xml, limit = 5) {
title,
url: parsedUrl.href,
domain: source || parsedUrl.hostname.replace(/^www\./, ''),
publishedAt: Number.isNaN(Date.parse(rawDate)) ? null : new Date(rawDate).toISOString(),
publishedAt: Number.isNaN(Date.parse(rawDate))
? null
: new Date(rawDate).toISOString(),
sourceCountry: null,
});
if (articles.length >= limit) break;
@@ -42,7 +59,8 @@ function normalizeRssArticles(xml, limit = 5) {
async function fetchRegionalNews(place) {
const query = place?.locality || place?.region || place?.country;
if (!query) return { status: 'unavailable', query: null, articles: [], source: null };
if (!query)
return { status: 'unavailable', query: null, articles: [], source: null };
const rssParams = new URLSearchParams({
q: String(query).replace(/["\\]/g, ' ').trim(),
hl: 'en-US',
@@ -50,13 +68,19 @@ async function fetchRegionalNews(place) {
ceid: 'US:en',
});
try {
const xml = await fetchRegionalText(`https://news.google.com/rss/search?${rssParams}`, {
const xml = await fetchRegionalText(
`https://news.google.com/rss/search?${rssParams}`,
{
headers: { 'User-Agent': 'GodsEyeView/0.1' },
timeoutMs: 12_000,
});
},
);
const articles = normalizeRssArticles(xml, 5);
if (articles.length) return { status: 'ready', query, articles, source: 'Google News RSS' };
} catch { /* fall through to the existing free index */ }
if (articles.length)
return { status: 'ready', query, articles, source: 'Google News RSS' };
} catch {
/* fall through to the existing free index */
}
const params = new URLSearchParams({
query: `"${String(query).replace(/["\\]/g, ' ').trim()}"`,
mode: 'artlist',
@@ -66,12 +90,20 @@ async function fetchRegionalNews(place) {
timespan: '48h',
});
try {
const payload = await fetchRegionalJson(`https://api.gdeltproject.org/api/v2/doc/doc?${params}`, {
const payload = await fetchRegionalJson(
`https://api.gdeltproject.org/api/v2/doc/doc?${params}`,
{
headers: { 'User-Agent': 'GodsEyeView/0.1' },
timeoutMs: 12_000,
});
},
);
const articles = normalizeRegionalArticles(payload, 5);
return { status: articles.length ? 'ready' : 'empty', query, articles, source: 'GDELT fallback' };
return {
status: articles.length ? 'ready' : 'empty',
query,
articles,
source: 'GDELT fallback',
};
} catch {
return { status: 'unavailable', query, articles: [], source: null };
}
+7 -3
View File
@@ -18,12 +18,16 @@ function fetchRegionalPlace(point) {
addressdetails: '1',
'accept-language': 'en',
});
const payload = await fetchRegionalJson(`https://nominatim.openstreetmap.org/reverse?${params}`, {
const payload = await fetchRegionalJson(
`https://nominatim.openstreetmap.org/reverse?${params}`,
{
headers: {
'User-Agent': 'GodsEyeView/0.1 (+https://github.com/bilawalsidhu/gods-eye-view)',
'User-Agent':
'GodsEyeView/0.1 (+https://github.com/bilawalsidhu/gods-eye-view)',
Referer: 'https://github.com/bilawalsidhu/gods-eye-view',
},
});
},
);
return normalizeRegionalPlace(payload);
});
_nominatimQueue = task.catch(() => null);
+2 -1
View File
@@ -4,7 +4,8 @@ function validRegionalPoint(params) {
const latitude = requiredFiniteQueryNumber(params, 'latitude');
const longitude = requiredFiniteQueryNumber(params, 'longitude');
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null;
if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) return null;
if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180)
return null;
return { latitude, longitude };
}
+26 -6
View File
@@ -13,7 +13,11 @@ const _weatherEffectsCache = new Map();
const _weatherEffectsInFlight = new Map();
const _weatherEffectsRateLimiter = makeRateLimiter({ windowMs: 60_000, max: 45, globalMax: 120 });
const _weatherEffectsRateLimiter = makeRateLimiter({
windowMs: 60_000,
max: 45,
globalMax: 120,
});
function trimWeatherEffectsCache() {
while (_weatherEffectsCache.size > WEATHER_EFFECTS_MAX_CACHE) {
@@ -46,7 +50,10 @@ function weatherEffectsProxy() {
return;
}
if (!_weatherEffectsRateLimiter(clientKey(req))) {
res.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': '10' });
res.writeHead(429, {
'Content-Type': 'application/json',
'Retry-After': '10',
});
res.end(JSON.stringify({ error: 'Rate limit exceeded' }));
return;
}
@@ -54,7 +61,11 @@ function weatherEffectsProxy() {
const point = validRegionalPoint(url.searchParams);
if (!point) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Valid latitude and longitude are required' }));
res.end(
JSON.stringify({
error: 'Valid latitude and longitude are required',
}),
);
return;
}
const key = `${(Math.round(point.latitude * 10) / 10).toFixed(1)},${(Math.round(point.longitude * 10) / 10).toFixed(1)}`;
@@ -69,7 +80,9 @@ function weatherEffectsProxy() {
res.end(JSON.stringify({ ...cached.payload, status: 'cached' }));
return;
}
const request = coalesceProxyRequest(_weatherEffectsInFlight, key, () => refresh(point, key));
const request = coalesceProxyRequest(_weatherEffectsInFlight, key, () =>
refresh(point, key),
);
try {
const payload = await request.promise;
res.writeHead(200, {
@@ -88,8 +101,15 @@ function weatherEffectsProxy() {
res.end(JSON.stringify({ ...cached.payload, status: 'stale' }));
return;
}
res.writeHead(503, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
res.end(JSON.stringify({ error: 'Weather effects are temporarily unavailable' }));
res.writeHead(503, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
res.end(
JSON.stringify({
error: 'Weather effects are temporarily unavailable',
}),
);
}
});
}
+7 -3
View File
@@ -7,13 +7,17 @@ async function fetchRegionalWeather(point) {
const params = new URLSearchParams({
latitude: point.latitude.toFixed(5),
longitude: point.longitude.toFixed(5),
current: 'temperature_2m,apparent_temperature,precipitation,weather_code,cloud_cover,wind_speed_10m,wind_direction_10m,visibility',
current:
'temperature_2m,apparent_temperature,precipitation,weather_code,cloud_cover,wind_speed_10m,wind_direction_10m,visibility',
timezone: 'UTC',
});
try {
const payload = await fetchRegionalJson(`https://api.open-meteo.com/v1/forecast?${params}`, {
const payload = await fetchRegionalJson(
`https://api.open-meteo.com/v1/forecast?${params}`,
{
maxBytes: WEATHER_EFFECTS_MAX_RESPONSE_BYTES,
});
},
);
return normalizeRegionalWeather(payload);
} catch {
return null;
+64 -21
View File
@@ -1,4 +1,11 @@
import { knownKeySetupEnvVars, admitKeySetupRequest, isKeySetupExternallyManaged, keySetupStatus, validateKeySetupUpdates, upsertDotenvValues } from '../../src/keySetupCore.mjs';
import {
knownKeySetupEnvVars,
admitKeySetupRequest,
isKeySetupExternallyManaged,
keySetupStatus,
validateKeySetupUpdates,
upsertDotenvValues,
} from '../../src/keySetupCore.mjs';
import { defaultSourceRoot } from '../providers/common/source-root.js';
import path from 'node:path';
import { readEnvironmentSource as readPinokioEnvironmentSource } from '../../scripts/pinokio-environment.mjs';
@@ -23,9 +30,15 @@ const LAUNCHER_AT_BOOT = process.env.GEV_LAUNCHER;
* Recomputing the snapshot there would classify the panel's own keys as
* external (read-only) until the whole process is relaunched.
*/
const PROVIDER_ENV_AT_BOOT = globalThis.__GEV_PROVIDER_ENV_AT_BOOT ??= Object.freeze(Object.fromEntries(
[...knownKeySetupEnvVars()].map((name) => [name, String(process.env[name] ?? '').trim()]),
));
const PROVIDER_ENV_AT_BOOT = (globalThis.__GEV_PROVIDER_ENV_AT_BOOT ??=
Object.freeze(
Object.fromEntries(
[...knownKeySetupEnvVars()].map((name) => [
name,
String(process.env[name] ?? '').trim(),
]),
),
));
/**
* `dev-fresh.sh` resolves dotenv and Keychain values before it starts Vite, so
@@ -79,8 +92,13 @@ function keySetupEndpoint({ sourceRoot = defaultSourceRoot } = {}) {
// `GEV_LAUNCHER=pinokio` line in someone's .env would silently redirect a
// plain `npm run dev` to write the Pinokio store it never loaded.
const pinokioManaged = () => LAUNCHER_AT_BOOT === 'pinokio';
const storeName = () => (pinokioManaged() ? 'pinokio-environment' : 'env-file');
const storePath = () => path.join(sourceRoot, ...(pinokioManaged() ? ['pinokio', 'ENVIRONMENT'] : ['.env']));
const storeName = () =>
pinokioManaged() ? 'pinokio-environment' : 'env-file';
const storePath = () =>
path.join(
sourceRoot,
...(pinokioManaged() ? ['pinokio', 'ENVIRONMENT'] : ['.env']),
);
// Read the store, distinguishing "no store yet" from "cannot read this
// store". Only ENOENT means empty. Every other failure — a permission error,
// an I/O fault, an undecodable file — must ABORT the save: upserting into a
@@ -97,7 +115,9 @@ function keySetupEndpoint({ sourceRoot = defaultSourceRoot } = {}) {
return fs.readFileSync(storePath(), 'utf8');
} catch (error) {
if (error?.code === 'ENOENT') return ''; // The first saved key births the file.
const unreadable = new Error('the existing configuration could not be read, so nothing was changed');
const unreadable = new Error(
'the existing configuration could not be read, so nothing was changed',
);
unreadable.code = 'GEV_STORE_UNREADABLE';
throw unreadable;
}
@@ -114,7 +134,8 @@ function keySetupEndpoint({ sourceRoot = defaultSourceRoot } = {}) {
};
// The gate itself is pure and unit-tested (admitKeySetupRequest in
// src/keySetupCore.mjs) — this just feeds it the request.
const admit = (req) => admitKeySetupRequest({
const admit = (req) =>
admitKeySetupRequest({
method: req.method,
remoteAddress: req.socket?.remoteAddress,
hostHeader: req.headers?.host,
@@ -147,7 +168,9 @@ function keySetupEndpoint({ sourceRoot = defaultSourceRoot } = {}) {
// offered); 'external' = supplied by env/Keychain/another workflow
// (read-only — the panel must never rewrite or delete it).
key.managed = key.set
? (key.envVars.some((name) => isExternallyManaged(name, inStore)) ? 'external' : 'file')
? key.envVars.some((name) => isExternallyManaged(name, inStore))
? 'external'
: 'file'
: null;
}
return { ...status, store: storeName() };
@@ -161,7 +184,9 @@ function keySetupEndpoint({ sourceRoot = defaultSourceRoot } = {}) {
// Never write THROUGH a symlink into a credential path.
try {
if (fs.lstatSync(filepath).isSymbolicLink()) {
throw new Error('refusing to write a credential store that is a symlink');
throw new Error(
'refusing to write a credential store that is a symlink',
);
}
} catch (error) {
if (error.code !== 'ENOENT') throw error; // absent is fine — first save.
@@ -183,7 +208,9 @@ function keySetupEndpoint({ sourceRoot = defaultSourceRoot } = {}) {
// hardening failure aborts with the previous store fully intact and the
// secret never on disk unprotected — no rollback path to get wrong.
if (!hardenCredentialFile(tmp)) {
const error = new Error('could not restrict the credential file to your account; nothing was saved');
const error = new Error(
'could not restrict the credential file to your account; nothing was saved',
);
error.code = 'GEV_HARDEN_FAILED';
throw error;
}
@@ -215,18 +242,23 @@ function keySetupEndpoint({ sourceRoot = defaultSourceRoot } = {}) {
// only install via configureServer (never configurePreviewServer), so they
// are absent from preview today — but pinning apply here makes that a
// guarantee rather than an accident of which hook a future edit uses.
apply: (_config, { command, isPreview }) => command === 'serve' && !isPreview,
apply: (_config, { command, isPreview }) =>
command === 'serve' && !isPreview,
configureServer(server) {
server.middlewares.use('/api/setup/status', (req, res) => {
if (req.method !== 'GET') return respond(res, 405, { error: 'Method not allowed' });
if (req.method !== 'GET')
return respond(res, 405, { error: 'Method not allowed' });
const admission = admit(req);
if (!admission.ok) return respond(res, admission.status, { error: admission.error });
if (!admission.ok)
return respond(res, admission.status, { error: admission.error });
respond(res, 200, providerStatus());
});
server.middlewares.use('/api/setup/keys', (req, res) => {
if (req.method !== 'POST') return respond(res, 405, { error: 'Method not allowed' });
if (req.method !== 'POST')
return respond(res, 405, { error: 'Method not allowed' });
const admission = admit(req);
if (!admission.ok) return respond(res, admission.status, { error: admission.error });
if (!admission.ok)
return respond(res, admission.status, { error: admission.error });
let body = '';
let overflowed = false;
req.on('data', (chunk) => {
@@ -237,7 +269,8 @@ function keySetupEndpoint({ sourceRoot = defaultSourceRoot } = {}) {
}
});
req.on('end', () => {
if (overflowed) return respond(res, 413, { error: 'Request too large' });
if (overflowed)
return respond(res, 413, { error: 'Request too large' });
let parsed;
try {
parsed = JSON.parse(body || '{}');
@@ -266,10 +299,17 @@ function keySetupEndpoint({ sourceRoot = defaultSourceRoot } = {}) {
// "saved world-readable" must never be reported as a generic write
// error. Everything else returns a fixed message (a raw filesystem
// error can carry an absolute path; that stays in the server log).
if (error?.code === 'GEV_HARDEN_FAILED' || error?.code === 'GEV_STORE_UNREADABLE') {
return respond(res, 500, { error: `The key was not saved: ${error.message}` });
if (
error?.code === 'GEV_HARDEN_FAILED' ||
error?.code === 'GEV_STORE_UNREADABLE'
) {
return respond(res, 500, {
error: `The key was not saved: ${error.message}`,
});
}
return respond(res, 500, { error: `Could not write the ${storeName()} store` });
return respond(res, 500, {
error: `Could not write the ${storeName()} store`,
});
}
// Live for the server-side proxies immediately; the restart below is
// what re-injects the client-exposed defines (Google, Cesium ion).
@@ -289,7 +329,10 @@ function keySetupEndpoint({ sourceRoot = defaultSourceRoot } = {}) {
// .env watcher may fire too; a second queued restart is harmless.
setTimeout(() => {
server.restart().catch((error) => {
console.warn('[KeySetup] Dev-server restart failed:', error?.message || error);
console.warn(
'[KeySetup] Dev-server restart failed:',
error?.message || error,
);
});
}, 250);
});
+160 -33
View File
@@ -1,12 +1,21 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { Readable } from 'node:stream';
import { mkdtempSync, readFileSync, existsSync, rmSync, statSync } from 'node:fs';
import {
mkdtempSync,
readFileSync,
existsSync,
rmSync,
statSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { overpassProxy } from 'gods-eye-view/server/providers/overpass';
import { militaryInstallationsProxy } from 'gods-eye-view/server/providers/military-installations';
import { regionalBriefProxy, weatherEffectsProxy } from 'gods-eye-view/server/providers/regional';
import {
regionalBriefProxy,
weatherEffectsProxy,
} from 'gods-eye-view/server/providers/regional';
import { openAiRealtimeProxy } from 'gods-eye-view/server/providers/openai';
import { keySetupEndpoint } from 'gods-eye-view/server/standalone/key-setup';
import { realtimeInstructions } from '../../server/providers/openai/instructions.js';
@@ -20,24 +29,57 @@ function install(plugin, preview = false) {
});
return routes;
}
function request(handler, { method = 'GET', url = '/', body = '', origin = 'http://localhost:4173' } = {}) {
function request(
handler,
{
method = 'GET',
url = '/',
body = '',
origin = 'http://localhost:4173',
} = {},
) {
return new Promise((resolve, reject) => {
const req = Readable.from(body ? [Buffer.from(body)] : []);
Object.assign(req, { method, url, headers: { host: 'localhost:4173', origin, 'content-type': 'application/json' }, socket: { remoteAddress: '127.0.0.1' } });
Object.assign(req, {
method,
url,
headers: {
host: 'localhost:4173',
origin,
'content-type': 'application/json',
},
socket: { remoteAddress: '127.0.0.1' },
});
const headers = {};
const res = {
statusCode: 200,
setHeader(name, value) { headers[name.toLowerCase()] = value; },
writeHead(status, values) { this.statusCode = status; for (const [k, v] of Object.entries(values)) this.setHeader(k, v); },
end(body = '') { resolve({ status: this.statusCode, headers, body: String(body), json: () => JSON.parse(String(body)) }); },
setHeader(name, value) {
headers[name.toLowerCase()] = value;
},
writeHead(status, values) {
this.statusCode = status;
for (const [k, v] of Object.entries(values)) this.setHeader(k, v);
},
end(body = '') {
resolve({
status: this.statusCode,
headers,
body: String(body),
json: () => JSON.parse(String(body)),
});
},
};
Promise.resolve(handler(req, res)).catch(reject);
});
}
function env(t, name, value) {
const old = process.env[name];
if (value === undefined) delete process.env[name]; else process.env[name] = value;
t.after(() => { if (old === undefined) delete process.env[name]; else process.env[name] = old; });
if (value === undefined) delete process.env[name];
else process.env[name] = value;
t.after(() => {
if (old === undefined) delete process.env[name];
else process.env[name] = old;
});
}
function root(t) {
const dir = mkdtempSync(path.join(tmpdir(), 'gev-services-'));
@@ -46,12 +88,29 @@ function root(t) {
}
test('standalone service guards run in development and preview without upstream acquisition', async (t) => {
t.mock.method(globalThis, 'fetch', () => { throw Error('invalid requests must not fetch'); });
t.mock.method(globalThis, 'fetch', () => {
throw Error('invalid requests must not fetch');
});
for (const preview of [false, true]) {
for (const [factory, route] of [[overpassProxy, '/api/overpass'], [militaryInstallationsProxy, '/api/military-installations'], [regionalBriefProxy, '/api/regional-brief'], [weatherEffectsProxy, '/api/weather-effects']]) {
for (const [factory, route] of [
[overpassProxy, '/api/overpass'],
[militaryInstallationsProxy, '/api/military-installations'],
[regionalBriefProxy, '/api/regional-brief'],
[weatherEffectsProxy, '/api/weather-effects'],
]) {
const routes = install(factory(), preview);
assert.equal((await request(routes.get(route), { method: 'DELETE' })).status, 405);
assert.equal((await request(routes.get(route), { method: route === '/api/overpass' ? 'POST' : 'GET' })).status, 400);
assert.equal(
(await request(routes.get(route), { method: 'DELETE' })).status,
405,
);
assert.equal(
(
await request(routes.get(route), {
method: route === '/api/overpass' ? 'POST' : 'GET',
})
).status,
400,
);
}
}
});
@@ -64,17 +123,36 @@ test('weather-only requests share upstream work and retain fresh and stale respo
calls++;
assert.equal(new URL(url).hostname, 'api.open-meteo.com');
if (calls > 1) throw Error('offline');
await new Promise(resolve => setTimeout(resolve, 10));
return Response.json({ current: { time: '2026-09-12T12:00', temperature_2m: 20, weather_code: 0, wind_speed_10m: 10 } });
await new Promise((resolve) => setTimeout(resolve, 10));
return Response.json({
current: {
time: '2026-09-12T12:00',
temperature_2m: 20,
weather_code: 0,
wind_speed_10m: 10,
},
});
});
const handler = install(weatherEffectsProxy()).get('/api/weather-effects');
const query = { url: '/?latitude=34.61&longitude=-112.43' };
const pair = await Promise.all([request(handler, query), request(handler, query)]);
assert.deepEqual(pair.map(r => r.headers['x-weather-effects']).sort(), ['INFLIGHT', 'MISS']);
const pair = await Promise.all([
request(handler, query),
request(handler, query),
]);
assert.deepEqual(pair.map((r) => r.headers['x-weather-effects']).sort(), [
'INFLIGHT',
'MISS',
]);
assert.equal(calls, 1);
assert.equal((await request(handler, query)).headers['x-weather-effects'], 'HIT');
assert.equal(
(await request(handler, query)).headers['x-weather-effects'],
'HIT',
);
now += 6 * 60_000;
assert.equal((await request(handler, query)).headers['x-weather-effects'], 'STALE');
assert.equal(
(await request(handler, query)).headers['x-weather-effects'],
'STALE',
);
});
test('Realtime handler preserves tools and default instructions, isolates supplied annotation guidance, and keeps the upstream key server-side', async (t) => {
@@ -83,17 +161,33 @@ test('Realtime handler preserves tools and default instructions, isolates suppli
const sent = [];
t.mock.method(globalThis, 'fetch', async (url, options) => {
assert.equal(url, 'https://api.openai.com/v1/realtime/client_secrets');
assert.equal(options.headers.Authorization, 'Bearer fixture-upstream-secret');
assert.equal(
options.headers.Authorization,
'Bearer fixture-upstream-secret',
);
sent.push(JSON.parse(options.body));
return Response.json({ value: 'fixture-ephemeral' });
});
for (const [options, guidance] of [[{}, undefined], [{ annotationGuidance: 'Fixture annotation instruction.' }, 'Fixture annotation instruction.'], [{}, undefined]]) {
const response = await request(install(openAiRealtimeProxy(options)).get('/api/realtime/token'), { url: '/?tier=unknown' });
for (const [options, guidance] of [
[{}, undefined],
[
{ annotationGuidance: 'Fixture annotation instruction.' },
'Fixture annotation instruction.',
],
[{}, undefined],
]) {
const response = await request(
install(openAiRealtimeProxy(options)).get('/api/realtime/token'),
{ url: '/?tier=unknown' },
);
assert.equal(response.status, 200);
assert.equal(response.headers['x-gev-voice-tier'], 'standard');
assert.equal(response.headers['x-gev-voice-tier-fallback'], '1');
assert.equal(response.body.includes('fixture-upstream-secret'), false);
assert.equal(sent.at(-1).session.instructions, realtimeInstructions(guidance));
assert.equal(
sent.at(-1).session.instructions,
realtimeInstructions(guidance),
);
assert.deepEqual(sent.at(-1).session.tools, GEV_REALTIME_TOOLS);
}
assert.notEqual(sent[0].session.instructions, sent[1].session.instructions);
@@ -101,30 +195,63 @@ test('Realtime handler preserves tools and default instructions, isolates suppli
});
test('debug logging resolves each supplied application directory independently', async (t) => {
const first = root(t), second = root(t);
for (const [sourceRoot, marker] of [[first, 'first'], [second, 'second']]) {
const handler = install(openAiRealtimeProxy({ sourceRoot })).get('/api/realtime/debug-log');
assert.equal((await request(handler, { method: 'POST', body: JSON.stringify({ marker }) })).status, 204);
const file = path.join(sourceRoot, '.gev-logs/realtime-conversations.jsonl');
const first = root(t),
second = root(t);
for (const [sourceRoot, marker] of [
[first, 'first'],
[second, 'second'],
]) {
const handler = install(openAiRealtimeProxy({ sourceRoot })).get(
'/api/realtime/debug-log',
);
assert.equal(
(
await request(handler, {
method: 'POST',
body: JSON.stringify({ marker }),
})
).status,
204,
);
const file = path.join(
sourceRoot,
'.gev-logs/realtime-conversations.jsonl',
);
assert.equal(JSON.parse(readFileSync(file, 'utf8')).marker, marker);
}
});
test('key setup writes only the supplied application root, retains request guards and stays absent from preview', async (t) => {
const first = root(t), untouched = root(t);
const first = root(t),
untouched = root(t);
env(t, 'OPENAI_API_KEY', undefined);
const plugin = keySetupEndpoint({ sourceRoot: first });
assert.equal(plugin.apply({}, { command: 'serve', isPreview: true }), false);
assert.equal(plugin.configurePreviewServer, undefined);
const routes = install(plugin);
const handler = routes.get('/api/setup/keys');
const body = JSON.stringify({ OPENAI_API_KEY: 'sk-fixture-only-not-a-real-key' });
assert.equal((await request(handler, { method: 'POST', body, origin: 'https://example.com' })).status, 403);
const body = JSON.stringify({
OPENAI_API_KEY: 'sk-fixture-only-not-a-real-key',
});
assert.equal(
(
await request(handler, {
method: 'POST',
body,
origin: 'https://example.com',
})
).status,
403,
);
assert.equal(existsSync(path.join(first, '.env')), false);
const saved = await request(handler, { method: 'POST', body });
assert.equal(saved.status, 200);
assert.match(readFileSync(path.join(first, '.env'), 'utf8'), /OPENAI_API_KEY=sk-fixture-only-not-a-real-key/);
if (process.platform !== 'win32') assert.equal(statSync(path.join(first, '.env')).mode & 0o777, 0o600);
assert.match(
readFileSync(path.join(first, '.env'), 'utf8'),
/OPENAI_API_KEY=sk-fixture-only-not-a-real-key/,
);
if (process.platform !== 'win32')
assert.equal(statSync(path.join(first, '.env')).mode & 0o777, 0o600);
assert.equal(saved.body.includes('sk-fixture-only-not-a-real-key'), false);
assert.equal(existsSync(path.join(untouched, '.env')), false);
});