88// adsb.lol serves data, sends no access-control-allow-origin
99// adsb.fi serves data, sends no access-control-allow-origin
1010// OpenSky allows only its own origin
11+ // AirLabs sends the header, but registration is closed
1112//
1213// A browser cannot work around any of that: CORS is enforced by the browser
13- // and only the API's owner can relax it. So the feed is now pluggable, and the
14- // one provider still answering browsers - AirLabs - is used when the visitor
15- // supplies a key.
14+ // and only the API's owner can relax it. So the feed is pluggable, and the
15+ // working route is now a shim the visitor hosts themselves - see
16+ // `worker/index.js`, which fetches from adsb.lol and adds the one header a
17+ // browser needs.
1618//
17- // Keys are supplied by the visitor and kept in their own localStorage. None is
18- // bundled. That is partly hygiene, and partly courtesy: this repository is
19- // public and permissively licensed, so a shipped key would become everyone's
20- // key and land the provider with traffic from every fork at once.
19+ // Nothing is bundled and no endpoint of ours is shared. This repository is
20+ // public and permissively licensed, so anything shipped here would become
21+ // everyone's, and land whichever service it points at with traffic from every
22+ // fork at once.
2123
2224import type { Config } from "@shared/index.js" ;
2325
@@ -53,9 +55,6 @@ export interface Provider {
5355 parse ( json : unknown ) : RawAircraft [ ] ;
5456}
5557
56- const M_PER_FOOT = 0.3048 ;
57- const KMH_PER_KNOT = 1.852 ;
58-
5958/**
6059 * airplanes.live - the original feed.
6160 *
@@ -77,88 +76,50 @@ export const airplanesLive: Provider = {
7776} ;
7877
7978/**
80- * AirLabs - the fallback, and as of August 2026 the only free service still
81- * sending `access-control-allow-origin: *`.
79+ * A CORS shim you host yourself.
80+ *
81+ * adsb.lol and adsb.fi still serve the data Vector was built around; they just
82+ * do not send `access-control-allow-origin`, so a browser discards it. A tiny
83+ * Cloudflare Worker in between adds the header and nothing else - see
84+ * `worker/index.js` in this repository, roughly 100 lines and free to run.
8285 *
83- * Needs a free key, which the visitor supplies. Its units differ from the
84- * ADS-B convention the rest of the app uses: metres and km/h rather than feet
85- * and knots, so both are converted here rather than leaking outward.
86+ * The URL is supplied by the visitor, because it is their Worker: nothing is
87+ * bundled, nothing is shared, and no quota of ours is spent by a fork.
8688 */
87- export const airlabs : Provider = {
88- id : "airlabs" ,
89- label : "AirLabs" ,
90- ready : ( cfg ) => Boolean ( cfg . apiKey ?. trim ( ) ) ,
91- // AirLabs takes a radius in kilometres; 250 nm keeps the two providers
92- // interchangeable and sits inside what it will answer.
89+ export const proxy : Provider = {
90+ id : "proxy" ,
91+ label : "adsb.lol" ,
92+ ready : ( cfg ) => Boolean ( cfg . feedProxy ?. trim ( ) ) ,
9393 maxRadiusNm : 250 ,
9494 url : ( cfg , radiusNm ) => {
95- const km = Math . round ( radiusNm * KMH_PER_KNOT ) ;
96- const key = encodeURIComponent ( cfg . apiKey ?. trim ( ) ?? "" ) ;
95+ const base = ( cfg . feedProxy ?? "" ) . trim ( ) . replace ( / \/ + $ / , "" ) ;
9796 return (
98- `https://airlabs.co/api/v9/flights ?lat=${ cfg . centerLat } ` +
99- `&lng =${ cfg . centerLon } &distance= ${ km } &api_key= ${ key } `
97+ `${ base } ?lat=${ cfg . centerLat } &lon= ${ cfg . centerLon } ` +
98+ `&radius =${ radiusNm } &source=adsb.lol `
10099 ) ;
101100 } ,
102101 parse : ( json ) => {
103102 const body = json as {
104- response ?: AirLabsFlight [ ] ;
105- error ?: { message ?: string } ;
103+ ac ?: RawAircraft [ ] ;
104+ aircraft ?: RawAircraft [ ] ;
105+ error ?: string ;
106106 } ;
107- if ( body . error ) throw new Error ( body . error . message ?? "AirLabs error" ) ;
108- const list = body . response ?? [ ] ;
109-
110- return list . map ( ( f ) => ( {
111- hex : f . hex ,
112- flight : f . flight_icao ?? f . flight_iata ?? f . flight_number ,
113- lat : f . lat ,
114- lon : f . lng ,
115- // AirLabs reports altitude in metres; the app works in feet.
116- alt_baro :
117- typeof f . alt === "number" ? Math . round ( f . alt / M_PER_FOOT ) : undefined ,
118- // ...and ground speed in km/h, where the app works in knots.
119- gs : typeof f . speed === "number" ? f . speed / KMH_PER_KNOT : undefined ,
120- track : f . dir ,
121- baro_rate :
122- typeof f . v_speed === "number"
123- ? Math . round ( ( f . v_speed / M_PER_FOOT ) * 60 )
124- : undefined ,
125- r : f . reg_number ,
126- t : f . aircraft_icao ,
127- seen : f . updated ? Math . max ( 0 , Date . now ( ) / 1000 - f . updated ) : undefined ,
128- } ) ) ;
107+ if ( body . error ) throw new Error ( body . error ) ;
108+ // adsb.lol answers in the same shape as airplanes.live, so nothing
109+ // downstream has to know which one it is reading.
110+ return body . ac ?? body . aircraft ?? [ ] ;
129111 } ,
130112} ;
131113
132- /** The subset of AirLabs' flight record this app consumes. */
133- interface AirLabsFlight {
134- hex ?: string ;
135- reg_number ?: string ;
136- lat ?: number ;
137- lng ?: number ;
138- /** Altitude in metres. */
139- alt ?: number ;
140- dir ?: number ;
141- /** Ground speed in km/h. */
142- speed ?: number ;
143- /** Vertical speed in metres per second. */
144- v_speed ?: number ;
145- flight_number ?: string ;
146- flight_icao ?: string ;
147- flight_iata ?: string ;
148- aircraft_icao ?: string ;
149- /** Unix seconds. */
150- updated ?: number ;
151- }
152-
153- export const PROVIDERS : Provider [ ] = [ airlabs , airplanesLive ] ;
114+ export const PROVIDERS : Provider [ ] = [ proxy , airplanesLive ] ;
154115
155116/**
156117 * Pick the provider to poll.
157118 *
158- * A supplied key means the visitor has chosen AirLabs deliberately, so it wins.
159- * Without one there is only airplanes.live, which will fail while its block
160- * stands - but failing against the original feed reports something truer than
161- * failing against a service the visitor never configured.
119+ * A configured proxy means the visitor has set one up deliberately, so it
120+ * wins. Without one there is only airplanes.live, which will fail while its
121+ * block stands - but failing against the original feed reports something truer
122+ * than failing against a service the visitor never configured.
162123 */
163124export function selectProvider ( cfg : Config ) : Provider {
164125 return PROVIDERS . find ( ( p ) => p . ready ( cfg ) ) ?? airplanesLive ;
0 commit comments