import { getOpenSkyToken } from './opensky.js'; import { readCappedResponseText } from '../common/http.js'; /** * Vite plugin: aircraft track-history backfill proxies (PRD WS-F F1/F2). * * /api/flights/track?icao24= — OpenSky GET /tracks/all (experimental; * own credit bucket, 4 credits per call on the free tier). OAuth via the * shared coalesced token. 60s per-icao cache; 404/429 forwarded so the * client can fall back to its accumulated trail silently. * /api/military/track?hex= — compatibility refusal for the retired, * undocumented ADSB.lol trace route. The standalone military source uses * locally accumulated trails; provider-supplied history remains optional. */ export function trackBackfillProxies() { const TRACK_CACHE_MS = 60000; const TRACK_CACHE_MAX = 200; const RESPONSE_CAP_BYTES = 5 * 1024 * 1024; /** @type {Map} */ const cache = new Map(); function cachePut(key, entry) { cache.set(key, entry); if (cache.size > TRACK_CACHE_MAX) { const oldest = [...cache.entries()].sort((a, b) => a[1].at - b[1].at)[0]; if (oldest) cache.delete(oldest[0]); } } async function proxyJson(res, key, upstreamUrl, headers = {}) { const cached = cache.get(key); if (cached && Date.now() - cached.at < TRACK_CACHE_MS) { res.statusCode = cached.status; res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.setHeader('Cache-Control', 'no-store'); res.end(cached.body); return; } const upstream = await fetch(upstreamUrl, { headers, signal: AbortSignal.timeout(12000), }); const { tooLarge, text } = await readCappedResponseText( upstream, RESPONSE_CAP_BYTES, ); let body; let status = upstream.status; if (tooLarge) { status = 502; body = JSON.stringify({ error: 'Upstream track response too large' }); } else if (!upstream.ok) { // Sanitize upstream error surface; status code is signal enough body = JSON.stringify({ error: `Track source HTTP ${upstream.status}` }); } else { body = text; } // Cache the 502 like any upstream error: /tracks/all costs OpenSky // credits, so a retry inside the window must not re-download the body. cachePut(key, { at: Date.now(), status, body }); res.statusCode = status; res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.setHeader('Cache-Control', 'no-store'); res.end(body); } function install(middlewares) { middlewares.use('/api/flights/track', async (req, res) => { try { const incoming = new URL(req.url || '', 'http://localhost'); const icao24 = String(incoming.searchParams.get('icao24') || '') .trim() .toLowerCase(); if (!/^[0-9a-f]{6}$/.test(icao24)) { res.statusCode = 400; res.setHeader('Content-Type', 'application/json'); res.end( JSON.stringify({ error: 'icao24 must be a 6-char hex string' }), ); return; } const token = await getOpenSkyToken(); await proxyJson( res, `osky:${icao24}`, `https://opensky-network.org/api/tracks/all?icao24=${icao24}&time=0`, token ? { Authorization: `Bearer ${token}` } : {}, ); } catch (error) { res.statusCode = 502; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ error: 'OpenSky track fetch failed' })); } }); middlewares.use('/api/military/track', async (req, res) => { try { const incoming = new URL(req.url || '', 'http://localhost'); const hex = String(incoming.searchParams.get('hex') || '') .trim() .toLowerCase(); if (!/^[0-9a-f~]{6,7}$/.test(hex)) { res.statusCode = 400; res.setHeader('Content-Type', 'application/json'); res.end( JSON.stringify({ error: 'hex must be a 6-7 char hex string' }), ); return; } res.statusCode = 503; res.setHeader('Content-Type', 'application/json'); res.setHeader('Cache-Control', 'no-store'); res.setHeader('Retry-After', '1800'); res.end( JSON.stringify({ error: 'Military history unavailable; locally accumulated trails remain available', code: 'history_unavailable', }), ); } catch (error) { res.statusCode = 502; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ error: 'adsb.lol trace fetch failed' })); } }); } return { name: 'track-backfill-proxies', configureServer(server) { install(server.middlewares); }, configurePreviewServer(server) { install(server.middlewares); }, }; }