Files
gods-eye-view/src/data/gbfsSource.js
T
ulihhhhandClaude Opus 5.5 194595e1b6 feat(bikeshare): add Barcelona's Bicing and AMBici
- Bicing (543 stations, PBSC) and AMBici (237 e-bike stations across the
  metropolitan area, nextbike) join the GBFS registry from the operators'
  keyless GBFS 2.3 feeds, both listed in MobilityData's systems catalogue.
- gbfs.nextbike.net joins the proxy allowlist as an exact host.
- Credits, DATA_SOURCES, CURRENT-STATE and CHANGELOG updated; a test pins
  each system's proxy reachability and the range check for its outermost
  stations.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-16 10:54:58 +02:00

108 lines
2.9 KiB
JavaScript

/** Allowlisted GBFS hostnames; wildcard *.publicbikesystem.net also accepted. */
const GBFS_ALLOWED_HOSTS = new Set([
'gbfs.lyft.com',
'gbfs.bluebikes.com',
'gbfs.bcycle.com',
'gbfs.biketownpdx.com',
'reddybikeshare.socialbicycles.com',
'austin.publicbikesystem.net',
'hon.publicbikesystem.net',
'chat.publicbikesystem.net',
'gbfs.nextbike.net',
'gbfs.urbansharing.com',
'api.entur.io',
'api.cyclocity.fr',
]);
/**
* Hosts that serve more than GBFS keep the proxy to their GBFS tree: Entur's
* api.entur.io fronts every Entur API, and api.cyclocity.fr every JCDecaux
* contract endpoint.
*/
const GBFS_HOST_PATH_PREFIXES = new Map([
['api.entur.io', '/mobility/v2/gbfs/'],
['api.cyclocity.fr', '/contracts/'],
]);
/**
* Identifying headers an operator asks every GBFS client to send. Entur rate
* limits or blocks consumers that do not send ET-Client-Name, in the form
* "company-application".
*/
const GBFS_HOST_HEADERS = new Map([
[
'api.entur.io',
Object.freeze({ 'ET-Client-Name': 'gods-eye-view-bikeshare' }),
],
]);
/**
* Check whether a hostname is in the GBFS allowlist.
*
* Also accepts any subdomain of publicbikesystem.net.
*
* @param {string} hostname
* @returns {boolean}
*/
export function isAllowedGbfsHost(hostname) {
const host = String(hostname || '')
.trim()
.toLowerCase();
if (!host) return false;
if (GBFS_ALLOWED_HOSTS.has(host)) return true;
return host.endsWith('.publicbikesystem.net');
}
/**
* Only allow station_information and station_status endpoints. The `.json`
* suffix is optional because Entur serves its feeds without one. When a
* hostname is given and that host serves more than GBFS, the path must also
* sit under the host's GBFS prefix.
*
* @param {string} pathname
* @param {string} [hostname]
* @returns {boolean}
*/
export function isAllowedGbfsPath(pathname, hostname = '') {
const path = String(pathname || '');
if (!/\/station_(information|status)(\.json)?$/i.test(path)) return false;
const prefix = GBFS_HOST_PATH_PREFIXES.get(
String(hostname || '')
.trim()
.toLowerCase(),
);
return !prefix || path.startsWith(prefix);
}
/**
* Extra request headers an upstream GBFS host asks clients to send.
*
* @param {string} hostname
* @returns {Record<string, string>}
*/
export function gbfsUpstreamHeaders(hostname) {
return (
GBFS_HOST_HEADERS.get(
String(hostname || '')
.trim()
.toLowerCase(),
) || {}
);
}
/**
* Return an appropriate Cache-Control header for a GBFS endpoint.
*
* station_information is semi-static (5 min cache); station_status is
* real-time (no-store).
*
* @param {string} pathname
* @returns {string} Cache-Control header value.
*/
export function gbfsCacheControl(pathname) {
if (/\/station_information(\.json)?$/i.test(String(pathname || ''))) {
return 'public, max-age=300';
}
return 'no-store';
}