From 7da812eddd1ad4f131645e8c6a9f4f367a7b067a Mon Sep 17 00:00:00 2001 From: DeliberateEnsemble Date: Sun, 13 Sep 2026 23:06:51 -0400 Subject: [PATCH 1/2] fix: report keyless OpenSky access in setup doctor --- CHANGELOG.md | 5 +++++ README.md | 7 ++++--- docs/CURRENT-STATE.md | 1 + scripts/setup-doctor.mjs | 32 ++++++++++++++++++++++++++++---- src/setupDoctor.test.mjs | 38 +++++++++++++++++++++++++++++++++++++- 5 files changed, 75 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21c308b78..f0aa59192 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,11 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md ### Fixed +- Make `npm run doctor` report keyless anonymous OpenSky access for explicit + `OPENSKY_AUTH_MODE=anon` and missing OAuth client pairs while retaining the + existing OAuth-pair capability wording. OpenSky proxy authentication is + unchanged. + - Traffic now retries a failed destination after city navigation without a layer toggle. Camera departure cancels pending work, arrival checks the final view, and superseded requests cannot keep a newer view loading. diff --git a/README.md b/README.md index 4135028b5..f79fedd67 100644 --- a/README.md +++ b/README.md @@ -405,9 +405,10 @@ Add these if you need higher polling allowances. where each configured provider was found without printing credential values. On macOS its Keychain-aware result previews `./scripts/dev-fresh.sh`; plain `npm run dev` reads only explicit environment and Vite dotenv values. The -OpenSky summary reports only OAuth client-pair presence, not the resolved -runtime mode or credential validity; Basic and credentials-file modes remain -advanced `dev-fresh.sh` configuration. +OpenSky summary reports keyless anonymous access for explicit `anon` or when +no OAuth client pair is present, and retains presence-only wording for a +complete OAuth pair unless `anon` is selected. Basic and credentials-file +modes remain advanced `dev-fresh.sh` configuration.
Advanced setup: environment variables and macOS Keychain diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index a009d68f7..7009f810f 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -2942,6 +2942,7 @@ are omitted rather than framing the wrong part of the globe. - Build gate: `npm run build` - Network access: local-only by default (`HOST=localhost` in dev-fresh.sh); LAN is an explicit opt-in via `HOST=0.0.0.0` (launcher prints a key-exposure warning + LAN URL; see SECURITY.md) - OpenSky default mode: OAuth (`OPENSKY_AUTH_MODE=oauth`; `anon` works without credentials) +- Setup doctor resolves `OPENSKY_AUTH_MODE` from the environment and dotenv files. Explicit `anon` and a missing OAuth client pair report keyless anonymous access (rate-limited); a complete OAuth pair retains the existing presence-only capability wording unless `anon` is selected. The proxy's auth behavior is unchanged. - Google key expected in Keychain service `google-maps-api` (or `GOOGLE_MAPS_API_KEY`, or `.env`) - OpenSky credentials expected in Keychain service `opensky-network` (or env, or `.env`); `OPENSKY_AUTH_MODE` and `OPENSKY_CREDENTIALS_FILE` read from `.env` too - Optional-key precedence in `dev-fresh.sh` is uniform — explicit shell env, then `.env`, then Keychain: `OPENAI_API_KEY` (Keychain `openai-api`/`api-key` — voice + HUD summary), `AISSTREAM_API_KEY` (`aisstream-api`/`api-key` — live vessels), `CESIUM_ION_TOKEN` (`cesium-ion`/`token` — Bing stacks), `TOMTOM_API_KEY` (`tomtom-api`/`api-key` — live traffic flow), `FIRMS_MAP_KEY` (`firms-map`/`map-key` — live fires), `LL2_API_TOKEN` (`.env` only) diff --git a/scripts/setup-doctor.mjs b/scripts/setup-doctor.mjs index 37c01156e..a9169839c 100644 --- a/scripts/setup-doctor.mjs +++ b/scripts/setup-doctor.mjs @@ -7,6 +7,8 @@ import { fileURLToPath } from 'node:url'; import { selectMapStartupRoute } from '../src/mapStartup.js'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const OPENSKY_AUTH_MODE_DEFAULT = 'oauth'; +const OPENSKY_AUTH_MODES = new Set(['basic', 'oauth', 'auto', 'anon']); export const CREDENTIALS = Object.freeze([ { name: 'GOOGLE_MAPS_API_KEY', label: 'Google Maps', keychain: [['google-maps-api', 'api-key'], ['google-maps-api', 'default'], ['google-maps-api', 'key']] }, @@ -100,6 +102,21 @@ export function readDoctorDotenvValue( return String(values[key] ?? ''); } +/** Resolve the OpenSky auth mode using the same environment and dotenv precedence as the launcher. */ +export function resolveOpenSkyAuthMode({ + authoritativeEnvironment = false, + environment = process.env, + rootDir = ROOT, +} = {}) { + const environmentDefinesMode = Object.prototype.hasOwnProperty.call(environment, 'OPENSKY_AUTH_MODE'); + const environmentMode = String(environment.OPENSKY_AUTH_MODE || '').trim(); + const dotenvMode = authoritativeEnvironment && environmentDefinesMode + ? '' + : readDoctorDotenvValue('OPENSKY_AUTH_MODE', rootDir); + const mode = (environmentMode || dotenvMode).trim().toLowerCase(); + return OPENSKY_AUTH_MODES.has(mode) ? mode : OPENSKY_AUTH_MODE_DEFAULT; +} + function hasKeychainItem(service, account) { if (process.platform !== 'darwin') return false; const result = spawnSync('security', [ @@ -127,8 +144,13 @@ export function resolveCredential(spec, { return { configured: false, source: null }; } -export function buildCapabilitySummary(credentials) { +export function buildCapabilitySummary( + credentials, + { openSkyAuthMode = OPENSKY_AUTH_MODE_DEFAULT } = {}, +) { const configured = (name) => credentials[name]?.configured === true; + const hasOAuthCredentials = configured('OPENSKY_CLIENT_ID') && configured('OPENSKY_CLIENT_SECRET'); + const anonymousOpenSkyMode = String(openSkyAuthMode || '').trim().toLowerCase() === 'anon'; const route = selectMapStartupRoute({ googleApiKey: configured('GOOGLE_MAPS_API_KEY') ? 'configured' : '', cesiumToken: configured('CESIUM_ION_TOKEN') ? 'configured' : '', @@ -139,9 +161,9 @@ export function buildCapabilitySummary(credentials) { : route === 'google-ion' ? 'Google Photorealistic 3D Tiles through Cesium ion; Bing and world-terrain stacks available' : 'Esri World Imagery (keyless satellite basemap) with keyless terrain', - flights: configured('OPENSKY_CLIENT_ID') && configured('OPENSKY_CLIENT_SECRET') + flights: hasOAuthCredentials && !anonymousOpenSkyMode ? 'OpenSky OAuth credentials present (runtime mode and validity not verified)' - : 'OpenSky OAuth credentials not configured', + : 'OpenSky keyless anonymous access (rate-limited)', voice: configured('OPENAI_API_KEY') ? 'available' : 'off until an OpenAI key is added', vessels: configured('AISSTREAM_API_KEY') ? 'live AISStream feed' : 'off until an AISStream key is added', fires: configured('FIRMS_MAP_KEY') ? 'live NASA FIRMS feed' : 'off until a FIRMS key is added', @@ -172,7 +194,9 @@ export function inspectSetup({ includeKeychain = true, authoritativeEnvironment : { available: false, version: null }, dependenciesInstalled, credentials, - capabilities: buildCapabilitySummary(credentials), + capabilities: buildCapabilitySummary(credentials, { + openSkyAuthMode: resolveOpenSkyAuthMode({ authoritativeEnvironment }), + }), }; } diff --git a/src/setupDoctor.test.mjs b/src/setupDoctor.test.mjs index 857a96c78..80720d5aa 100644 --- a/src/setupDoctor.test.mjs +++ b/src/setupDoctor.test.mjs @@ -12,6 +12,7 @@ import { isConfiguredValue, npmProcessSpec, readDoctorDotenvValue, + resolveOpenSkyAuthMode, resolveCredential, } from '../scripts/setup-doctor.mjs'; @@ -145,6 +146,41 @@ test('doctor reads the dotenv ladder without requiring Vite to be installed', () } }); +test('doctor resolves OpenSky auth mode with launcher-compatible precedence', () => { + const root = mkdtempSync(path.join(tmpdir(), 'gev-doctor-opensky-mode-')); + try { + writeFileSync(path.join(root, '.env'), 'OPENSKY_AUTH_MODE=anon\n'); + assert.equal(resolveOpenSkyAuthMode({ environment: {}, rootDir: root }), 'anon'); + assert.equal(resolveOpenSkyAuthMode({ environment: { OPENSKY_AUTH_MODE: 'oauth' }, rootDir: root }), 'oauth'); + assert.equal(resolveOpenSkyAuthMode({ environment: { OPENSKY_AUTH_MODE: '' }, rootDir: root }), 'anon'); + assert.equal(resolveOpenSkyAuthMode({ environment: { OPENSKY_AUTH_MODE: 'invalid' }, rootDir: root }), 'oauth'); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test('doctor reports keyless anonymous OpenSky access for explicit anon mode', () => { + const credentials = { + OPENSKY_CLIENT_ID: { configured: true }, + OPENSKY_CLIENT_SECRET: { configured: true }, + }; + assert.equal( + buildCapabilitySummary(credentials, { openSkyAuthMode: 'anon' }).flights, + 'OpenSky keyless anonymous access (rate-limited)', + ); +}); + +test('doctor retains OAuth capability wording for a complete client pair', () => { + const credentials = { + OPENSKY_CLIENT_ID: { configured: true }, + OPENSKY_CLIENT_SECRET: { configured: true }, + }; + assert.equal( + buildCapabilitySummary(credentials, { openSkyAuthMode: 'oauth' }).flights, + 'OpenSky OAuth credentials present (runtime mode and validity not verified)', + ); +}); + test('doctor describes the credential ladder without exposing values', () => { const credentials = { GOOGLE_MAPS_API_KEY: { configured: false }, @@ -163,7 +199,7 @@ test('doctor describes the credential ladder without exposing values', () => { assert.match(capabilities.map, /Bing and world-terrain stacks/); assert.equal(capabilities.voice, 'available'); assert.match(capabilities.missions, /token allowance/); - assert.equal(capabilities.flights, 'OpenSky OAuth credentials not configured'); + assert.equal(capabilities.flights, 'OpenSky keyless anonymous access (rate-limited)'); const report = formatSetupReport({ ready: true, From 9ad64112dceb0739a2ba8dd57efd5b96d8734c4a Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sun, 20 Sep 2026 16:43:16 -0700 Subject: [PATCH 2/2] fix: report OpenSky Basic and auto modes accurately --- CHANGELOG.md | 7 ++++--- README.md | 9 +++++---- docs/CURRENT-STATE.md | 2 +- scripts/setup-doctor.mjs | 18 ++++++++++++++---- src/setupDoctor.test.mjs | 26 ++++++++++++++++++++++++++ 5 files changed, 50 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46367a7ff..dde31f22e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -268,9 +268,10 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md ### Fixed - Make `npm run doctor` report keyless anonymous OpenSky access for explicit - `OPENSKY_AUTH_MODE=anon` and missing OAuth client pairs while retaining the - existing OAuth-pair capability wording. OpenSky proxy authentication is - unchanged. + `OPENSKY_AUTH_MODE=anon` and OAuth mode without a client pair, retain the + existing OAuth-pair capability wording, and identify selected Basic or auto + modes without guessing their eventual credential choice. OpenSky proxy + authentication is unchanged. - Bikeshare stations load again. The extracted station source addressed the proxy as `/api/gbfs?url=`, but the proxy reads its upstream target from the path, so every request answered 400 and the layer reported a fetch error for diff --git a/README.md b/README.md index cbb2490dd..580bb147c 100644 --- a/README.md +++ b/README.md @@ -414,10 +414,11 @@ Add these if you need higher polling allowances. where each configured provider was found without printing credential values. On macOS its Keychain-aware result previews `./scripts/dev-fresh.sh`; plain `npm run dev` reads only explicit environment and Vite dotenv values. The -OpenSky summary reports keyless anonymous access for explicit `anon` or when -no OAuth client pair is present, and retains presence-only wording for a -complete OAuth pair unless `anon` is selected. Basic and credentials-file -modes remain advanced `dev-fresh.sh` configuration. +OpenSky summary reports keyless anonymous access for explicit `anon` or an +OAuth mode without a client pair, retains presence-only wording for a complete +OAuth pair, and identifies selected Basic or auto mode without guessing which +credentials runtime will accept. Basic and credentials-file modes remain +advanced `dev-fresh.sh` configuration.
Advanced setup: environment variables and macOS Keychain diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index d72b08cce..c61abb3bd 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -3557,7 +3557,7 @@ are omitted rather than framing the wrong part of the globe. - Build gate: `npm run build` - Network access: local-only by default (`HOST=localhost` in dev-fresh.sh); LAN is an explicit opt-in via `HOST=0.0.0.0` (launcher prints a key-exposure warning + LAN URL; see SECURITY.md) - OpenSky default mode: OAuth (`OPENSKY_AUTH_MODE=oauth`; `anon` works without credentials) -- Setup doctor resolves `OPENSKY_AUTH_MODE` from the environment and dotenv files. Explicit `anon` and a missing OAuth client pair report keyless anonymous access (rate-limited); a complete OAuth pair retains the existing presence-only capability wording unless `anon` is selected. The proxy's auth behavior is unchanged. +- Setup doctor resolves `OPENSKY_AUTH_MODE` from the environment and dotenv files. Explicit `anon` and OAuth mode without a client pair report keyless anonymous access (rate-limited); a complete OAuth pair retains the existing presence-only capability wording. Basic and auto modes report the selected mode without guessing which credentials runtime will accept. The proxy's auth behavior is unchanged. - Google key expected in Keychain service `google-maps-api` (or `GOOGLE_MAPS_API_KEY`, or `.env`) - OpenSky credentials expected in Keychain service `opensky-network` (or env, or `.env`); `OPENSKY_AUTH_MODE` and `OPENSKY_CREDENTIALS_FILE` read from `.env` too - Optional-key precedence in `dev-fresh.sh` is uniform — explicit shell env, then `.env`, then Keychain: `OPENAI_API_KEY` (Keychain `openai-api`/`api-key` — voice + HUD summary), `AISSTREAM_API_KEY` (`aisstream-api`/`api-key` — live vessels), `CESIUM_ION_TOKEN` (`cesium-ion`/`token` — Bing stacks), `TOMTOM_API_KEY` (`tomtom-api`/`api-key` — live traffic flow), `FIRMS_MAP_KEY` (`firms-map`/`map-key` — live fires), `LL2_API_TOKEN` (`.env` only) diff --git a/scripts/setup-doctor.mjs b/scripts/setup-doctor.mjs index 6941bf4c3..954369030 100644 --- a/scripts/setup-doctor.mjs +++ b/scripts/setup-doctor.mjs @@ -151,7 +151,19 @@ export function buildCapabilitySummary( ) { const configured = (name) => credentials[name]?.configured === true; const hasOAuthCredentials = configured('OPENSKY_CLIENT_ID') && configured('OPENSKY_CLIENT_SECRET'); - const anonymousOpenSkyMode = String(openSkyAuthMode || '').trim().toLowerCase() === 'anon'; + const requestedOpenSkyMode = String(openSkyAuthMode || '').trim().toLowerCase(); + const openSkyMode = OPENSKY_AUTH_MODES.has(requestedOpenSkyMode) + ? requestedOpenSkyMode + : OPENSKY_AUTH_MODE_DEFAULT; + const flights = openSkyMode === 'anon' + ? 'OpenSky keyless anonymous access (rate-limited)' + : openSkyMode === 'basic' + ? 'OpenSky Basic mode selected (credential presence and validity not verified)' + : openSkyMode === 'auto' + ? 'OpenSky auto mode selected (runtime credential choice and validity not verified)' + : hasOAuthCredentials + ? 'OpenSky OAuth credentials present (runtime mode and validity not verified)' + : 'OpenSky keyless anonymous access (rate-limited)'; const route = selectMapStartupRoute({ googleApiKey: configured('GOOGLE_MAPS_API_KEY') ? 'configured' : '', cesiumToken: configured('CESIUM_ION_TOKEN') ? 'configured' : '', @@ -162,9 +174,7 @@ export function buildCapabilitySummary( : route === 'google-ion' ? 'Google Photorealistic 3D Tiles through Cesium ion; Bing and world-terrain stacks available' : 'Esri World Imagery (keyless satellite basemap) with keyless terrain', - flights: hasOAuthCredentials && !anonymousOpenSkyMode - ? 'OpenSky OAuth credentials present (runtime mode and validity not verified)' - : 'OpenSky keyless anonymous access (rate-limited)', + flights, voice: configured('OPENAI_API_KEY') ? 'available' : 'off until an OpenAI key is added', vessels: configured('AISSTREAM_API_KEY') ? 'live AISStream feed' : 'off until an AISStream key is added', fires: configured('FIRMS_MAP_KEY') ? 'live NASA FIRMS feed' : 'off until a FIRMS key is added', diff --git a/src/setupDoctor.test.mjs b/src/setupDoctor.test.mjs index 80720d5aa..c66d9e686 100644 --- a/src/setupDoctor.test.mjs +++ b/src/setupDoctor.test.mjs @@ -181,6 +181,32 @@ test('doctor retains OAuth capability wording for a complete client pair', () => ); }); +test('doctor reports Basic mode without inferring runtime auth from OAuth credentials', () => { + const oauthCredentials = { + OPENSKY_CLIENT_ID: { configured: true }, + OPENSKY_CLIENT_SECRET: { configured: true }, + }; + for (const credentials of [{}, oauthCredentials]) { + assert.equal( + buildCapabilitySummary(credentials, { openSkyAuthMode: 'basic' }).flights, + 'OpenSky Basic mode selected (credential presence and validity not verified)', + ); + } +}); + +test('doctor reports auto mode without assuming its eventual credential choice', () => { + const oauthCredentials = { + OPENSKY_CLIENT_ID: { configured: true }, + OPENSKY_CLIENT_SECRET: { configured: true }, + }; + for (const credentials of [{}, oauthCredentials]) { + assert.equal( + buildCapabilitySummary(credentials, { openSkyAuthMode: 'auto' }).flights, + 'OpenSky auto mode selected (runtime credential choice and validity not verified)', + ); + } +}); + test('doctor describes the credential ladder without exposing values', () => { const credentials = { GOOGLE_MAPS_API_KEY: { configured: false },