Merge remote-tracking branch 'upstream/main' into fix/363-geocode-proxy

# Conflicts:
#	SECURITY.md
#	server/providers/places/google.js
This commit is contained in:
Niccolò Saggese committed 2026-10-05 09:26:08 +02:00
commit e2ce7fa38e
223 files changed
+20759 -1752

No files matched your search

+30 -8
View File
@@ -3,7 +3,8 @@ import {
keylessGooglePlacesResponse,
keylessGoogleGeocodeResponse,
} from './google-key.js';
import { makeOptInRateLimiter, clientKey } from '../common/rate-limit.js';
import { makeCostRateLimiter, clientKey } from '../common/rate-limit.js';
import { admitSameSite } from '../common/same-site.js';
import { readResponseJsonCapped } from '../common/http.js';
import {
projectNearbyPlaces,
@@ -11,14 +12,27 @@ import {
} from '../../../src/data/placeProviderPayloads.js';
// Construct lazily after the standalone environment has loaded.
// undefined = not built yet; null = unlimited; fn = active limiter
// undefined = not built yet; null = the explicit 0 opt-out; fn = active limiter
let _googleRateLimiter;
/** Google cost endpoint (nearby-places). Null = unlimited (default). */
/**
* Requests/min/IP applied when GEV_RATELIMIT_GOOGLE_PER_MIN is unset. This is
* the value the Pinokio build already ships (pinokio/_ENVIRONMENT), so the
* packaged app keeps behaving exactly as it does today and only an
* unconfigured server changes — from unlimited to what the product already
* runs with. Places calls are user-driven (a search, a nearby lookup, an
* installation probe) rather than polled, so this sits far above what the app
* generates and bites only a caller enumerating the endpoint. `.env.example`
* suggests a tighter 60 when the host is not localhost; that still applies.
*/
export const GOOGLE_DEFAULT_PER_MIN = 120;
/** Google cost endpoints (nearby-places, text-search, geocoding). Null only when set to 0. */
function googleRateLimiter() {
if (_googleRateLimiter === undefined)
_googleRateLimiter = makeOptInRateLimiter(
_googleRateLimiter = makeCostRateLimiter(
process.env.GEV_RATELIMIT_GOOGLE_PER_MIN,
GOOGLE_DEFAULT_PER_MIN,
);
return _googleRateLimiter;
}
@@ -97,6 +111,9 @@ export function googlePlacesContextProxy({
} = {}) {
function install(middlewares) {
middlewares.use('/api/google/nearby-places', async (req, res) => {
// Gate first, like the OpenAI routes: a cross-site caller learns nothing
// about this endpoint's method surface.
if (admitSameSite(req, res)) return;
if (req.method !== 'GET') {
res.statusCode = 405;
res.setHeader('Content-Type', 'application/json');
@@ -127,7 +144,7 @@ export function googlePlacesContextProxy({
}
const { latitude, longitude } = coordinates;
// Opt-in per-IP throttle (GEV_RATELIMIT_GOOGLE_PER_MIN). No-op when unset.
// Per-IP throttle (GEV_RATELIMIT_GOOGLE_PER_MIN). On by default; 0 disables.
// Inlined (not the shared helper) so the 429 body keeps this endpoint's
// `places: []` contract that the client expects on every error response.
const _grl = googleRateLimiter();
@@ -208,6 +225,8 @@ export function googlePlacesContextProxy({
// a view-biased Text Search lands on the actual feature. Same key, field
// mask, throttle, and `places: []` error contract as nearby-places above.
middlewares.use('/api/google/text-search', async (req, res) => {
// Paid like nearby-places, so gated the same way, first.
if (admitSameSite(req, res)) return;
if (req.method !== 'GET') {
res.statusCode = 405;
res.setHeader('Content-Type', 'application/json');
@@ -247,7 +266,7 @@ export function googlePlacesContextProxy({
}
const { latitude, longitude } = coordinates;
// Opt-in per-IP throttle (GEV_RATELIMIT_GOOGLE_PER_MIN). No-op when unset.
// Per-IP throttle (GEV_RATELIMIT_GOOGLE_PER_MIN). On by default; 0 disables.
// Inlined (like nearby-places) so the 429 body keeps the `places: []`
// contract the client expects on every error response.
const _grl = googleRateLimiter();
@@ -344,6 +363,9 @@ export function googlePlacesContextProxy({
);
};
// Gate first, like the Places routes above: a cross-site caller learns
// nothing about this endpoint's method surface.
if (admitSameSite(req, res)) return;
if (req.method !== 'GET') {
refuse(405, 'Method not allowed');
return;
@@ -369,8 +391,8 @@ export function googlePlacesContextProxy({
return;
}
// Opt-in per-IP throttle (GEV_RATELIMIT_GOOGLE_PER_MIN), shared with
// the Places routes because both spend the same Google budget.
// Per-IP throttle (GEV_RATELIMIT_GOOGLE_PER_MIN), shared with the
// Places routes because both spend the same Google budget.
const _grl = googleRateLimiter();
if (_grl && !_grl(clientKey(req))) {
res.statusCode = 429;