Merge pull request #444 from vortsghost2025/fix/issue-362-opensky-doctor

fix: report OpenSky anonymous mode in setup doctor
This commit is contained in:
Sameh Khamis authored and GitHub committed 2026-09-20 16:47:59 -07:00
commit a5f820bb0d
5 files changed
+112 -8

No files matched your search

+5
View File
@@ -267,6 +267,11 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
### Fixed ### Fixed
- Make `npm run doctor` report keyless anonymous OpenSky access for explicit
`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 - Bikeshare stations load again. The extracted station source addressed the
proxy as `/api/gbfs?url=`, but the proxy reads its upstream target from 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 path, so every request answered 400 and the layer reported a fetch error for
+4 -2
View File
@@ -414,8 +414,10 @@ Add these if you need higher polling allowances.
where each configured provider was found without printing credential values. where each configured provider was found without printing credential values.
On macOS its Keychain-aware result previews `./scripts/dev-fresh.sh`; plain On macOS its Keychain-aware result previews `./scripts/dev-fresh.sh`; plain
`npm run dev` reads only explicit environment and Vite dotenv values. The `npm run dev` reads only explicit environment and Vite dotenv values. The
OpenSky summary reports only OAuth client-pair presence, not the resolved OpenSky summary reports keyless anonymous access for explicit `anon` or an
runtime mode or credential validity; Basic and credentials-file modes remain 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 `dev-fresh.sh` configuration.
<details> <details>
+1
View File
@@ -3557,6 +3557,7 @@ are omitted rather than framing the wrong part of the globe.
- Build gate: `npm run build` - 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) - 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) - 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 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`) - 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 - 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) - 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)
+39 -5
View File
@@ -8,6 +8,8 @@ import { projectRoot } from './project-root.mjs';
import { selectMapStartupRoute } from '../src/mapStartup.js'; import { selectMapStartupRoute } from '../src/mapStartup.js';
const ROOT = projectRoot(import.meta.url); const ROOT = projectRoot(import.meta.url);
const OPENSKY_AUTH_MODE_DEFAULT = 'oauth';
const OPENSKY_AUTH_MODES = new Set(['basic', 'oauth', 'auto', 'anon']);
export const CREDENTIALS = Object.freeze([ 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']] }, { name: 'GOOGLE_MAPS_API_KEY', label: 'Google Maps', keychain: [['google-maps-api', 'api-key'], ['google-maps-api', 'default'], ['google-maps-api', 'key']] },
@@ -101,6 +103,21 @@ export function readDoctorDotenvValue(
return String(values[key] ?? ''); 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) { function hasKeychainItem(service, account) {
if (process.platform !== 'darwin') return false; if (process.platform !== 'darwin') return false;
const result = spawnSync('security', [ const result = spawnSync('security', [
@@ -128,8 +145,25 @@ export function resolveCredential(spec, {
return { configured: false, source: null }; 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 configured = (name) => credentials[name]?.configured === true;
const hasOAuthCredentials = configured('OPENSKY_CLIENT_ID') && configured('OPENSKY_CLIENT_SECRET');
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({ const route = selectMapStartupRoute({
googleApiKey: configured('GOOGLE_MAPS_API_KEY') ? 'configured' : '', googleApiKey: configured('GOOGLE_MAPS_API_KEY') ? 'configured' : '',
cesiumToken: configured('CESIUM_ION_TOKEN') ? 'configured' : '', cesiumToken: configured('CESIUM_ION_TOKEN') ? 'configured' : '',
@@ -140,9 +174,7 @@ export function buildCapabilitySummary(credentials) {
: route === 'google-ion' : route === 'google-ion'
? 'Google Photorealistic 3D Tiles through Cesium ion; Bing and world-terrain stacks available' ? 'Google Photorealistic 3D Tiles through Cesium ion; Bing and world-terrain stacks available'
: 'Esri World Imagery (keyless satellite basemap) with keyless terrain', : 'Esri World Imagery (keyless satellite basemap) with keyless terrain',
flights: configured('OPENSKY_CLIENT_ID') && configured('OPENSKY_CLIENT_SECRET') flights,
? 'OpenSky OAuth credentials present (runtime mode and validity not verified)'
: 'OpenSky OAuth credentials not configured',
voice: configured('OPENAI_API_KEY') ? 'available' : 'off until an OpenAI key is added', 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', 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', fires: configured('FIRMS_MAP_KEY') ? 'live NASA FIRMS feed' : 'off until a FIRMS key is added',
@@ -173,7 +205,9 @@ export function inspectSetup({ includeKeychain = true, authoritativeEnvironment
: { available: false, version: null }, : { available: false, version: null },
dependenciesInstalled, dependenciesInstalled,
credentials, credentials,
capabilities: buildCapabilitySummary(credentials), capabilities: buildCapabilitySummary(credentials, {
openSkyAuthMode: resolveOpenSkyAuthMode({ authoritativeEnvironment, rootDir }),
}),
}; };
} }
+63 -1
View File
@@ -12,6 +12,7 @@ import {
isConfiguredValue, isConfiguredValue,
npmProcessSpec, npmProcessSpec,
readDoctorDotenvValue, readDoctorDotenvValue,
resolveOpenSkyAuthMode,
resolveCredential, resolveCredential,
} from '../scripts/setup-doctor.mjs'; } from '../scripts/setup-doctor.mjs';
@@ -145,6 +146,67 @@ 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 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', () => { test('doctor describes the credential ladder without exposing values', () => {
const credentials = { const credentials = {
GOOGLE_MAPS_API_KEY: { configured: false }, GOOGLE_MAPS_API_KEY: { configured: false },
@@ -163,7 +225,7 @@ test('doctor describes the credential ladder without exposing values', () => {
assert.match(capabilities.map, /Bing and world-terrain stacks/); assert.match(capabilities.map, /Bing and world-terrain stacks/);
assert.equal(capabilities.voice, 'available'); assert.equal(capabilities.voice, 'available');
assert.match(capabilities.missions, /token allowance/); 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({ const report = formatSetupReport({
ready: true, ready: true,