diff --git a/server/providers/overpass/osmFeaturesRoute.js b/server/providers/overpass/osmFeaturesRoute.js index d36c3b2e3..b9423eb8e 100644 --- a/server/providers/overpass/osmFeaturesRoute.js +++ b/server/providers/overpass/osmFeaturesRoute.js @@ -1,6 +1,6 @@ import path from 'node:path'; import { makeRateLimiter, clientKey } from '../common/rate-limit.js'; -import { admitSameSite } from '../common/same-site.js'; +import { admitSameSiteKeyless } from '../common/same-site.js'; import { createBoundedCache } from '../common/boundedCache.js'; import { compileOsmFeatureQuery, @@ -201,8 +201,9 @@ export function installOsmFeaturesRoute( middlewares.use('/api/osm/features', async (req, res) => { const send = sender(res); if (req.method !== 'GET') return send(jsonError(405, 'Method Not Allowed')); - // Quota-bearing: a page elsewhere must not spend the operator's Overpass. - if (admitSameSite(req, res)) return undefined; + // A page elsewhere must not spend the operator's Overpass. The keyless + // gate, as on /api/overpass: voice works behind a reverse proxy too. + if (admitSameSiteKeyless(req, res)) return undefined; if (!configured()) return send( jsonError(503, 'Place search needs a configured Overpass server.', { diff --git a/src/tooling/osmFeaturesRoute.test.mjs b/src/tooling/osmFeaturesRoute.test.mjs index d3b122435..d4e078e5b 100644 --- a/src/tooling/osmFeaturesRoute.test.mjs +++ b/src/tooling/osmFeaturesRoute.test.mjs @@ -380,6 +380,14 @@ test('a cross-site page cannot spend the operator Overpass', async () => { 'sec-fetch-site': 'same-origin', }); assert.equal(own.status, 200); + // Like /api/overpass, it works behind a reverse proxy: forwarding headers + // alone are not a refusal. + const proxied = await request('preset=school&bbox=0,0,2,2', undefined, { + host: 'localhost:5173', + 'sec-fetch-site': 'same-origin', + 'x-forwarded-for': '203.0.113.7', + }); + assert.equal(proxied.status, 200); }); test('an upstream refusal is not an empty answer', async () => { diff --git a/src/voice/pointAndAsk.test.mjs b/src/voice/pointAndAsk.test.mjs index 9d5a3d0a5..47f985e1d 100644 --- a/src/voice/pointAndAsk.test.mjs +++ b/src/voice/pointAndAsk.test.mjs @@ -346,7 +346,8 @@ test('deixis prompt lint: one short rule, and every sentinel it names exists in // it to 51,646. Voice geometry (resolve_area, find_imagery, osm_query and // the analyst area scopes) raised the ceiling to 55,500; tightening the // AREAS rule and the area tool descriptions left it at 55,263 with an - // operator Overpass, a deliberate ~240-character margin. A new tool or rule + // operator Overpass; main's Sea Marks and Ski Pistes layers bring it to + // 55,370, ~130 under. A new tool or rule // has to fit under this ceiling. const size = instructions.length + JSON.stringify(GEV_REALTIME_TOOLS).length; assert.ok(size <= 55_500, `prompt size ${size}`);