fix: report keyless OpenSky access in setup doctor

This commit is contained in:
DeliberateEnsemble committed 2026-09-13 23:06:51 -04:00
1 parent 0272635f25
commit 7da812eddd
5 files changed
+75 -8

No files matched your search

+5
View File
@@ -67,6 +67,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 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 - Traffic now retries a failed destination after city navigation without a layer
toggle. Camera departure cancels pending work, arrival checks the final view, toggle. Camera departure cancels pending work, arrival checks the final view,
and superseded requests cannot keep a newer view loading. and superseded requests cannot keep a newer view loading.
+4 -3
View File
@@ -405,9 +405,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 when
runtime mode or credential validity; Basic and credentials-file modes remain no OAuth client pair is present, and retains presence-only wording for a
advanced `dev-fresh.sh` configuration. complete OAuth pair unless `anon` is selected. Basic and credentials-file
modes remain advanced `dev-fresh.sh` configuration.
<details> <details>
<summary>Advanced setup: environment variables and macOS Keychain</summary> <summary>Advanced setup: environment variables and macOS Keychain</summary>
+1
View File
@@ -2942,6 +2942,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 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`) - 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)
+28 -4
View File
@@ -7,6 +7,8 @@ import { fileURLToPath } from 'node:url';
import { selectMapStartupRoute } from '../src/mapStartup.js'; import { selectMapStartupRoute } from '../src/mapStartup.js';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); 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([ 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']] },
@@ -100,6 +102,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', [
@@ -127,8 +144,13 @@ 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 anonymousOpenSkyMode = String(openSkyAuthMode || '').trim().toLowerCase() === 'anon';
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' : '',
@@ -139,9 +161,9 @@ 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: hasOAuthCredentials && !anonymousOpenSkyMode
? 'OpenSky OAuth credentials present (runtime mode and validity not verified)' ? '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', 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',
@@ -172,7 +194,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 }),
}),
}; };
} }
+37 -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,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', () => { 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 +199,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,