/** * `GET /api/v1/flights` — for a city, not for the box. * * `?city=socal`, or `?lat=&lng=` resolved against the same allowlist the weather * route uses, or neither for the default region. `regions.ts` owns the * validation, the refusal, and the reasoning behind refusing at all: a live * traffic endpoint that will fetch any coordinate on demand is an amplifier * pointed at a volunteer-funded feed. * * Publicly cacheable **when the licence allows it**, because the whole design of * the plan is that one response serves every viewer of a region for its whole * TTL. Aircraft are not personal data and this body never varies by who asked — * only by where. * * The condition is not hypothetical caution. Handing a body to a shared cache is * republication: the CDN serves it to people this box never spoke to, under * whatever credit the body carries. So the answer comes from the body's own * `redistributable` flag, which `flights/licence.ts` derived from the terms of * the feed that answered — and a body that may not be shared simply keeps the * fail-closed `private, no-store` every reply starts with (CONTRACT.md §5). * * ### `radiusNm` on the query is ignored, deliberately * * The browser sends one. It is dropped, and the size of the circle stays * `TERA_ADSB_RADIUS_NM`, for a reason that is not stubbornness: the cache key * would have to include it, and a caller who can choose the key can make this * box hold an unbounded number of entries and issue an unbounded number of * distinct upstream requests — each one more expensive than the last, since a * wider circle is more work for the feed to answer. Every bound in * `flights/index.ts` and `regions.ts` rests on the key space being the operator's * region list, and a query parameter that widens it dissolves all of them. * * An operator whose board is bigger than the circle raises * `TERA_ADSB_RADIUS_NM`; 60 nm covers both shipped cities. The client already * discards aircraft outside the region it drew, so a circle that is too large * costs a little bandwidth and nothing else. */ import type { FastifyInstance } from "fastify"; import { publicCache } from "../cache.ts"; import { mayRepublish } from "../flights/licence.ts"; import { resolveRegion, type RegionQuery } from "../regions.ts"; import type { ErrorBody } from "../../../src/server/wire.ts"; import type { Services } from "../services.ts"; export function registerFlights(app: FastifyInstance, services: Services): void { app.get<{ Querystring: RegionQuery }>("/api/v1/flights", async (req, reply) => { const resolved = resolveRegion(services.config.regions, req.query); if (!resolved.ok) { const error: ErrorBody = { error: "bad_request", message: resolved.message }; return reply.code(400).send(error); } const body = await services.flights.current(resolved.region); if (mayRepublish(body)) publicCache(req, reply, body.ttlSeconds); return body; }); }