import { normalizeAdsbLolPointResponse } from '../../../src/data/adsbLolFallback.js'; import { coalesceProxyRequest, readResponseJsonCapped, readResponseTextCapped, } from '../common/http.js'; import { requiredFiniteQueryNumber } from '../common/query.js'; // --------------------------------------------------------------------------- // OpenSky OAuth2 token + response cache state // --------------------------------------------------------------------------- /** @type {string|null} Current OAuth2 bearer token. */ let _openskyToken = null; /** @type {number} Epoch-ms when the current token expires. */ let _openskyTokenExpiry = 0; /** @type {Promise|null} In-flight token refresh promise (coalesces concurrent callers). */ let _openskyTokenPromise = null; /** @type {string|null} Cached upstream response body (JSON text). */ let _openskyCacheBody = null; /** @type {number} HTTP status of the cached response. */ let _openskyCacheStatus = 0; /** @type {number} Epoch-ms when the response was cached. */ let _openskyCacheTime = 0; /** @type {{requestedMode:string,usedMode:string,reason:string}|null} Auth metadata for the cached response. */ let _openskyCacheMeta = null; /** @type {number|null} Source snapshot epoch from the cached OpenSky body. */ let _openskyCacheSourceEpochMs = null; /** TTL for the OpenSky response cache (ms). */ const OPENSKY_CACHE_MS = 9000; /** Per-attempt limit for the global snapshot; it normally arrives in ~1.5 s. */ const OPENSKY_ATTEMPT_TIMEOUT_MS = 10_000; const OPENSKY_STATES_URL = 'https://opensky-network.org/api/states/all?extended=1'; /** states/all?extended=1 measures ~0.8 MB anonymously; bounded, not budgeted. */ const OPENSKY_STATES_MAX_RESPONSE_BYTES = 8 * 1024 * 1024; /** * Credit-governor cooldown for a 429: OpenSky's retry-after bounded to * 30 s … 30 min, or 2 min when the header is absent. * @param {Response} upstream Rate-limited response. * @returns {number} Cooldown (ms). */ function openSkyRateLimitCooldownMs(upstream) { // headers.get() gives null when absent, and Number(null) is 0, not NaN. const header = upstream.headers.get('x-rate-limit-retry-after-seconds'); const retryAfterSec = header == null || header.trim() === '' ? NaN : Number(header); return Math.min( Math.max( Number.isFinite(retryAfterSec) ? retryAfterSec * 1000 : 120_000, 30_000, ), 30 * 60_000, ); } /** * Fetch and read the global snapshot, giving each attempt * OPENSKY_ATTEMPT_TIMEOUT_MS and retrying once when an attempt times out or * the connection fails. An oversized body is not retried: it would fail again * and spend more credits. A second failure throws, which the proxy answers * from its stale cache or the regional fallback. * @param {Record} headers Request headers. * @returns {Promise<{upstream: Response, body: string}>} */ async function fetchOpenSkyStates(headers) { for (let attempt = 1; ; attempt++) { try { const signal = AbortSignal.timeout(OPENSKY_ATTEMPT_TIMEOUT_MS); const upstream = await fetch(OPENSKY_STATES_URL, { headers, signal }); if (upstream.status === 429) { // Start the cooldown from the headers and drop the rejected body // unread, so a stalled or oversized 429 body cannot skip it. _openskyCooldownUntil = Date.now() + openSkyRateLimitCooldownMs(upstream); void upstream.body?.cancel().catch(() => {}); return { upstream, body: JSON.stringify({ error: 'OpenSky rate limited.' }), }; } const body = await readResponseTextCapped( upstream, OPENSKY_STATES_MAX_RESPONSE_BYTES, signal, ); return { upstream, body }; } catch (error) { if (attempt >= 2 || error?.code === 'RESPONSE_TOO_LARGE') throw error; console.warn( '[OpenSky] global snapshot attempt failed, retrying:', error?.message || error, ); } } } // --- OpenSky credit governor (field-test fix 2026-07-06) ------------------- // The global /states/all this proxy fetches costs 4 CREDITS per call against // OpenSky's ~4000/day authenticated budget — a day with the app open burned // the whole quota in ~8h and the layer then hard-died until the daily reset // ("rate limited for 48h" owner report; auth itself was fine). Three levers: // 1. Adaptive TTL: OpenSky returns X-Rate-Limit-Remaining on success; as the // budget thins, the proxy stretches its cache TTL so a full day of // continuous use never exhausts it. // 2. 429 cooldown: honor X-Rate-Limit-Retry-After-Seconds — no upstream // attempts until it passes (bounded 30 s … 30 min). // 3. Serve-stale: while rate-limited/cooling, serve the last-good body (200 + // X-OpenSky-Stale) so the layer keeps rendering instead of dying. /** @type {number} Current adaptive TTL (ms) — starts at the base cache TTL. */ let _openskyTtlMs = OPENSKY_CACHE_MS; /** @type {number} Epoch-ms before which no upstream fetch is attempted. */ let _openskyCooldownUntil = 0; /** * Picks the cache TTL from the remaining daily credit budget. * Client polls every 30 s, so tiers ≤30 s cost the same 480 credits/h; the * later tiers stretch the day: >2400 → ~3 h of full freshness, then 30 s * (~2.5 h), 90 s (~5 h), 300 s (~8 h) ≈ 18+ h of continuous use per day. * @param {number} remaining - X-Rate-Limit-Remaining header value. * @returns {number} TTL in ms. */ function openskyAdaptiveTtlMs(remaining) { if (!Number.isFinite(remaining)) return OPENSKY_CACHE_MS; if (remaining > 2400) return OPENSKY_CACHE_MS; if (remaining > 1200) return 30_000; if (remaining > 400) return 90_000; return 300_000; } /** @type {boolean} Guards duplicate auth-failure warnings in logs. */ let _openskyAuthWarned = false; /** @type {boolean} Guards duplicate invalid-auth-mode warnings. */ let _openskyAuthModeWarned = false; /** Default auth mode when OPENSKY_AUTH_MODE env is unset. */ const OPENSKY_AUTH_MODE_DEFAULT = 'oauth'; /** Set of valid OPENSKY_AUTH_MODE values. */ const OPENSKY_AUTH_MODE_SET = new Set(['basic', 'oauth', 'auto', 'anon']); /** Regional civilian fallback cache, keyed by a coarse 0.25° view anchor. */ const _adsbLolPointCache = new Map(); /** Per-anchor single-flight map for concurrent regional fallback requests. */ const _adsbLolPointInFlight = new Map(); const ADSBLOL_POINT_CACHE_MS = 12000; const ADSBLOL_POINT_CACHE_MAX = 80; const ADSBLOL_POINT_RADIUS_NM = 250; const ADSBLOL_POINT_MAX_RESPONSE_BYTES = 8 * 1024 * 1024; // A 200 response can still contain an old OpenSky snapshot. Past this point // the viewport-scoped adsb.lol source is more honest and keeps local motion // current instead of coasting a stale worldwide frame indefinitely. const OPENSKY_SOURCE_STALE_MS = 120_000; /** * Obtain a valid OpenSky OAuth2 bearer token, refreshing if needed. * * Uses the client_credentials grant against the OpenSky Keycloak realm. * Concurrent callers share a single in-flight refresh promise so only * one token request is issued at a time. * * @returns {Promise} Bearer token string, or null if unavailable. */ export async function getOpenSkyToken() { const now = Date.now(); // Return cached token if still valid (with 60 s safety margin) if (_openskyToken && now < _openskyTokenExpiry - 60000) return _openskyToken; // Coalesce concurrent refresh requests — if a refresh is already in-flight, // return the same promise instead of issuing a duplicate token request if (_openskyTokenPromise) return _openskyTokenPromise; const clientId = process.env.OPENSKY_CLIENT_ID; const clientSecret = process.env.OPENSKY_CLIENT_SECRET; if (!clientId || !clientSecret) return null; // Wrap the async token fetch in a shared promise stored in _openskyTokenPromise _openskyTokenPromise = (async () => { try { const res = await fetch( 'https://auth.opensky-network.org/auth/realms/opensky-network/protocol/openid-connect/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, signal: AbortSignal.timeout(OPENSKY_ATTEMPT_TIMEOUT_MS), body: `grant_type=client_credentials&client_id=${encodeURIComponent(clientId)}&client_secret=${encodeURIComponent(clientSecret)}`, }, ); let data = null; try { data = await res.json(); } catch { data = null; } const accessToken = data?.access_token; const expiresIn = Number(data?.expires_in); if (!res.ok || !accessToken) { if (!_openskyAuthWarned) { const detail = data?.error_description || data?.error || `HTTP ${res.status}`; console.warn('[OpenSky] OAuth client_credentials failed:', detail); _openskyAuthWarned = true; } _openskyToken = null; _openskyTokenExpiry = 0; return null; } _openskyToken = accessToken; // Default to 1800 s (30 min) if expires_in is missing or non-finite _openskyTokenExpiry = Date.now() + (Number.isFinite(expiresIn) ? expiresIn : 1800) * 1000; console.log( '[OpenSky] OAuth token refreshed, expires in', Number.isFinite(expiresIn) ? expiresIn : 1800, 's', ); _openskyAuthWarned = false; return _openskyToken; } catch (err) { if (!_openskyAuthWarned) { console.warn( '[OpenSky] OAuth token request failed:', err?.message || String(err), ); _openskyAuthWarned = true; } _openskyToken = null; _openskyTokenExpiry = 0; return null; } finally { // Clear the shared promise so the next caller can start a fresh refresh _openskyTokenPromise = null; } })(); return _openskyTokenPromise; } /** * Validate and normalize the OPENSKY_AUTH_MODE env value. * * @param {string} value - Raw env value (e.g. 'basic', 'oauth', 'auto', 'anon'). * @returns {string} One of the valid mode strings, or the default ('oauth'). */ function normalizeOpenSkyAuthMode(value) { const raw = String(value || '') .trim() .toLowerCase(); if (!raw) return OPENSKY_AUTH_MODE_DEFAULT; if (OPENSKY_AUTH_MODE_SET.has(raw)) return raw; if (!_openskyAuthModeWarned) { console.warn( `[OpenSky] Invalid OPENSKY_AUTH_MODE="${raw}", defaulting to "${OPENSKY_AUTH_MODE_DEFAULT}"`, ); _openskyAuthModeWarned = true; } return OPENSKY_AUTH_MODE_DEFAULT; } /** * Build standard response headers for OpenSky proxy responses. * * Includes diagnostic X-OpenSky-* headers so the client can inspect * cache hit/miss status and which auth mode was actually used. * * @param {object} opts * @param {string} opts.cacheStatus - 'HIT', 'MISS', or 'STALE'. * @param {string} opts.requestedMode - The auth mode the config requested. * @param {string} opts.usedMode - The auth mode actually used for the upstream call. * @param {string} opts.reason - Human-readable reason string for diagnostics. * @returns {Record} Header object. */ function buildOpenSkyHeaders({ cacheStatus, requestedMode, usedMode, reason, staleSeconds, retryAfterSeconds, }) { const headers = { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', 'X-OpenSky-Cache': cacheStatus, 'X-OpenSky-Auth': usedMode, 'X-OpenSky-Auth-Mode-Requested': requestedMode, 'X-OpenSky-Auth-Mode-Used': usedMode, 'X-OpenSky-Auth-Reason': reason, }; // Credit-governor extras (field-test fix 2026-07-06): the client can show a // STALE cue / countdown without parsing the body. if (Number.isFinite(staleSeconds)) headers['X-OpenSky-Stale-Seconds'] = String(Math.round(staleSeconds)); if (Number.isFinite(retryAfterSeconds)) headers['X-OpenSky-Retry-After-Seconds'] = String( Math.round(retryAfterSeconds), ); return headers; } export function adsbLolFallbackAnchor(req) { const incoming = new URL(req?.url || '', 'http://localhost'); const latitude = requiredFiniteQueryNumber(incoming.searchParams, 'lat'); const longitude = requiredFiniteQueryNumber(incoming.searchParams, 'lon'); if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90) return null; if (!Number.isFinite(longitude) || longitude < -180 || longitude > 180) return null; return { latitude, longitude }; } async function fetchAdsbLolPointFallback(req) { const anchor = adsbLolFallbackAnchor(req); if (!anchor) return null; const roundedLat = Math.round(anchor.latitude * 4) / 4; const roundedLon = Math.round(anchor.longitude * 4) / 4; const cacheKey = `${roundedLat.toFixed(2)},${roundedLon.toFixed(2)}`; const cached = _adsbLolPointCache.get(cacheKey); const now = Date.now(); if (cached && now - cached.cachedAt < ADSBLOL_POINT_CACHE_MS) { return { ...cached, cacheStatus: 'HIT' }; } const request = coalesceProxyRequest( _adsbLolPointInFlight, cacheKey, async () => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); try { const upstream = await fetch( `https://api.adsb.lol/v2/lat/${roundedLat}/lon/${roundedLon}/dist/${ADSBLOL_POINT_RADIUS_NM}`, { headers: { Accept: 'application/json', 'User-Agent': 'gods-eye-view-adsblol-regional-fallback/1.0', }, signal: controller.signal, }, ); if (!upstream.ok) throw new Error(`upstream HTTP ${upstream.status}`); const payload = await readResponseJsonCapped( upstream, ADSBLOL_POINT_MAX_RESPONSE_BYTES, ); const normalized = normalizeAdsbLolPointResponse(payload); const record = { body: JSON.stringify(normalized), cachedAt: Date.now(), count: normalized.states.length, }; _adsbLolPointCache.delete(cacheKey); _adsbLolPointCache.set(cacheKey, record); while (_adsbLolPointCache.size > ADSBLOL_POINT_CACHE_MAX) { _adsbLolPointCache.delete(_adsbLolPointCache.keys().next().value); } return record; } finally { clearTimeout(timeoutId); } }, ); try { const record = await request.promise; return { ...record, cacheStatus: request.shared ? 'INFLIGHT' : 'MISS' }; } catch (error) { if (!request.shared && error?.name !== 'AbortError') { console.warn('[adsb.lol Flights Fallback]', error?.message || error); } return cached ? { ...cached, cacheStatus: 'STALE' } : null; } } async function serveAdsbLolPointFallback(req, res, requestedMode, reason) { const fallback = await fetchAdsbLolPointFallback(req); if (!fallback) return false; res.writeHead(200, { ...buildOpenSkyHeaders({ cacheStatus: fallback.cacheStatus, requestedMode, usedMode: 'adsblol-regional', reason, }), 'X-Flight-Source': 'adsb.lol', 'X-Flight-Coverage': `${ADSBLOL_POINT_RADIUS_NM}nm regional fallback`, 'X-Flight-Count': String(fallback.count), }); res.end(fallback.body); return true; } function openSkySourceEpochMs(body) { try { const seconds = Number(JSON.parse(body)?.time); return Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : null; } catch { return null; } } function openSkySourceIsStale(sourceEpochMs, now = Date.now()) { return ( Number.isFinite(sourceEpochMs) && now - sourceEpochMs > OPENSKY_SOURCE_STALE_MS ); } /** * Vite plugin: OpenSky Network proxy with multi-mode auth and response caching. * * Supports four auth modes controlled by OPENSKY_AUTH_MODE env: * - 'oauth' (default) — client_credentials bearer token * - 'basic' — HTTP Basic with OPENSKY_USERNAME / OPENSKY_PASSWORD * - 'auto' — try OAuth first, fall back to Basic, then anon * - 'anon' — no credentials * * Successful responses are cached for OPENSKY_CACHE_MS (~9 s). On * upstream failure the proxy serves a stale cached response if available, or * a bounded 250 nm adsb.lol point snapshot around the current view anchor. * * @returns {import('vite').Plugin} */ export function openSkyProxy() { const installMiddleware = (server) => { server.middlewares.use('/api/flights', async (req, res, next) => { try { // This mount also matches its /track route, served by its own handler. if (new URL(req.url || '/', 'http://localhost').pathname !== '/') return next(); res.setHeader('X-Feed-Source', 'OpenSky Network'); const requestedMode = normalizeOpenSkyAuthMode( process.env.OPENSKY_AUTH_MODE, ); const now = Date.now(); const inCooldown = now < _openskyCooldownUntil; // Fresh-enough cache (adaptive TTL) OR any cache during a 429 // cooldown: serve it without touching upstream. Stale-during-cooldown // is deliberate (credit governor): last-good planes beat a dead layer. if ( _openskyCacheBody && (now - _openskyCacheTime < _openskyTtlMs || inCooldown) ) { if ( openSkySourceIsStale(_openskyCacheSourceEpochMs, now) && (await serveAdsbLolPointFallback( req, res, requestedMode, 'opensky_snapshot_stale_regional_fallback', )) ) { return; } const cachedMeta = _openskyCacheMeta || { requestedMode, usedMode: 'unknown', reason: 'cached', }; const isStale = now - _openskyCacheTime >= _openskyTtlMs; res.writeHead( _openskyCacheStatus || 200, buildOpenSkyHeaders({ cacheStatus: isStale ? 'STALE' : 'HIT', requestedMode: cachedMeta.requestedMode || requestedMode, usedMode: cachedMeta.usedMode || 'unknown', reason: isStale ? 'rate_limited_serving_stale' : cachedMeta.reason || 'cached', staleSeconds: isStale ? (now - _openskyCacheTime) / 1000 : undefined, retryAfterSeconds: inCooldown ? (_openskyCooldownUntil - now) / 1000 : undefined, }), ); res.end(_openskyCacheBody); return; } // Cooling down with nothing cached (cold start into a rate limit): // synthesize the 429 locally — hammering upstream mid-cooldown can't // succeed and just burns goodwill. if (inCooldown) { if ( await serveAdsbLolPointFallback( req, res, requestedMode, 'opensky_cooldown_regional_fallback', ) ) return; res.writeHead( 429, buildOpenSkyHeaders({ cacheStatus: 'COOLDOWN', requestedMode, usedMode: 'none', reason: 'rate_limited', retryAfterSeconds: (_openskyCooldownUntil - now) / 1000, }), ); res.end( JSON.stringify({ error: 'OpenSky rate limited; proxy cooling down.', }), ); return; } const basicUser = process.env.OPENSKY_USERNAME || ''; const basicPass = process.env.OPENSKY_PASSWORD || ''; const hasBasicCreds = Boolean(basicUser && basicPass); const headers = { Accept: 'application/json' }; let usedMode = 'anon'; let reason = 'forced_anonymous'; if (requestedMode === 'basic') { if (hasBasicCreds) { headers.Authorization = `Basic ${Buffer.from(`${basicUser}:${basicPass}`).toString('base64')}`; usedMode = 'basic'; reason = 'basic_credentials'; } else { reason = 'missing_basic_creds'; } } else if (requestedMode === 'oauth') { const token = await getOpenSkyToken(); if (token) { headers.Authorization = `Bearer ${token}`; usedMode = 'oauth'; reason = 'oauth_token'; } else { reason = 'oauth_invalid_or_missing'; } } else if (requestedMode === 'auto') { const token = await getOpenSkyToken(); if (token) { headers.Authorization = `Bearer ${token}`; usedMode = 'oauth'; reason = 'oauth_token'; } else if (hasBasicCreds) { headers.Authorization = `Basic ${Buffer.from(`${basicUser}:${basicPass}`).toString('base64')}`; usedMode = 'basic'; reason = 'oauth_unavailable_fallback_basic'; } else { reason = 'missing_oauth_and_basic_creds'; } } let { upstream, body } = await fetchOpenSkyStates(headers); // Auto-mode fallback: if OAuth was rejected, retry with Basic credentials if ( (upstream.status === 401 || upstream.status === 403) && requestedMode === 'auto' && usedMode === 'oauth' && hasBasicCreds ) { const retryHeaders = { Accept: 'application/json', Authorization: `Basic ${Buffer.from(`${basicUser}:${basicPass}`).toString('base64')}`, }; ({ upstream, body } = await fetchOpenSkyStates(retryHeaders)); usedMode = 'basic'; reason = 'oauth_rejected_fallback_basic'; } const sourceEpochMs = upstream.ok ? openSkySourceEpochMs(body) : null; if ( upstream.ok && openSkySourceIsStale(sourceEpochMs, now) && (await serveAdsbLolPointFallback( req, res, requestedMode, 'opensky_snapshot_stale_regional_fallback', )) ) { // Keep the last global snapshot available as a fail-soft cache, // but do not label or render it as a fresh live result. _openskyCacheBody = body; _openskyCacheStatus = upstream.status; _openskyCacheTime = now; _openskyCacheSourceEpochMs = sourceEpochMs; _openskyCacheMeta = { requestedMode, usedMode, reason }; return; } if (upstream.status === 429) { reason = 'rate_limited'; // Credit governor: fetchOpenSkyStates already started the cooldown // from OpenSky's retry-after — no upstream attempts until then. const cooldownMs = openSkyRateLimitCooldownMs(upstream); // Serve the last-good body instead of the 429 when we have one — // the layer keeps rendering (STALE-cued) instead of dying. if (_openskyCacheBody && _openskyCacheStatus === 200) { res.writeHead( 200, buildOpenSkyHeaders({ cacheStatus: 'STALE', requestedMode, usedMode, reason: 'rate_limited_serving_stale', staleSeconds: (now - _openskyCacheTime) / 1000, retryAfterSeconds: cooldownMs / 1000, }), ); res.end(_openskyCacheBody); return; } } if (!upstream.ok && !_openskyCacheBody) { const servedFallback = await serveAdsbLolPointFallback( req, res, requestedMode, `opensky_http_${upstream.status}_regional_fallback`, ); if (servedFallback) return; } if (upstream.status === 401 || upstream.status === 403) { if (requestedMode === 'basic' && !hasBasicCreds) { body = JSON.stringify({ error: 'OpenSky auth missing. Basic mode requires OPENSKY_USERNAME and OPENSKY_PASSWORD.', }); reason = 'missing_basic_creds'; } else if (requestedMode === 'oauth' && usedMode !== 'oauth') { body = JSON.stringify({ error: 'OpenSky auth invalid. OAuth mode requires valid OPENSKY_CLIENT_ID and OPENSKY_CLIENT_SECRET.', }); reason = 'oauth_invalid_or_missing'; } else if (usedMode === 'basic') { body = JSON.stringify({ error: 'OpenSky auth invalid. Username/password were rejected.', }); reason = 'basic_invalid_credentials'; } else if (usedMode === 'oauth') { body = JSON.stringify({ error: 'OpenSky auth invalid. OAuth client credentials were rejected.', }); reason = 'oauth_invalid_credentials'; } else if (requestedMode === 'auto' && !hasBasicCreds) { body = JSON.stringify({ error: 'OpenSky auth missing. Provide basic credentials or valid OAuth client credentials.', }); reason = 'missing_oauth_and_basic_creds'; } else { body = JSON.stringify({ error: 'OpenSky auth required.', }); reason = 'auth_required'; } } // Refine the reason string to reflect the actual outcome if (upstream.ok && reason === 'forced_anonymous') { reason = 'anonymous_ok'; } else if ( upstream.ok && usedMode === 'basic' && reason === 'basic_credentials' ) { reason = 'basic_ok'; } else if ( upstream.ok && usedMode === 'oauth' && reason === 'oauth_token' ) { reason = 'oauth_ok'; } // Only cache successful responses — error responses (401/403/429/5xx) // should not be served from cache on subsequent requests if (upstream.ok) { _openskyCacheBody = body; _openskyCacheStatus = upstream.status; _openskyCacheTime = now; _openskyCacheSourceEpochMs = sourceEpochMs; _openskyCacheMeta = { requestedMode, usedMode, reason, }; // Credit governor: adapt the cache TTL to the remaining daily // budget so a continuously-open app stretches its polls instead of // exhausting the quota mid-day. Success also clears any cooldown. const remaining = Number( upstream.headers.get('x-rate-limit-remaining'), ); _openskyTtlMs = openskyAdaptiveTtlMs(remaining); _openskyCooldownUntil = 0; } res.writeHead( upstream.status, buildOpenSkyHeaders({ cacheStatus: 'MISS', requestedMode, usedMode, reason, }), ); res.end(body); } catch (e) { console.error('[OpenSky Proxy]', e.message); if (_openskyCacheBody) { const cachedMeta = _openskyCacheMeta || { requestedMode: normalizeOpenSkyAuthMode( process.env.OPENSKY_AUTH_MODE, ), usedMode: 'unknown', reason: 'cached_stale', }; res.writeHead( _openskyCacheStatus || 200, buildOpenSkyHeaders({ cacheStatus: 'STALE', requestedMode: cachedMeta.requestedMode || OPENSKY_AUTH_MODE_DEFAULT, usedMode: cachedMeta.usedMode || 'unknown', reason: cachedMeta.reason || 'cached_stale', }), ); res.end(_openskyCacheBody); return; } const requestedMode = normalizeOpenSkyAuthMode( process.env.OPENSKY_AUTH_MODE, ); if ( await serveAdsbLolPointFallback( req, res, requestedMode, 'opensky_proxy_error_regional_fallback', ) ) return; res.writeHead( 502, buildOpenSkyHeaders({ cacheStatus: 'MISS', requestedMode, usedMode: 'error', reason: 'proxy_error', }), ); res.end(JSON.stringify({ error: 'OpenSky proxy error' })); } }); }; return { name: 'opensky-proxy', configureServer: installMiddleware, configurePreviewServer: installMiddleware, }; }