mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Merge pull request #444 from vortsghost2025/fix/issue-362-opensky-doctor
fix: report OpenSky anonymous mode in setup doctor
This commit is contained in:
5 files changed
+112
-8
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 }),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in new issue
Block a user