From cb546e4a72dfdafc8af972eb80f1a5e92e33a968 Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 29 Oct 2026 09:42:17 +0100 Subject: [PATCH] refactor(server): move the identified Places request into a file of its own The instance token and the request that carries the installation key leave trek-places.client.ts, which had grown past the 1000-line limit. --- server/src/nest/maps/trek-places.client.ts | 46 +-------------------- server/src/nest/maps/trek-places.fetch.ts | 48 ++++++++++++++++++++++ 2 files changed, 50 insertions(+), 44 deletions(-) create mode 100644 server/src/nest/maps/trek-places.fetch.ts diff --git a/server/src/nest/maps/trek-places.client.ts b/server/src/nest/maps/trek-places.client.ts index eee8314dc..07beda067 100644 --- a/server/src/nest/maps/trek-places.client.ts +++ b/server/src/nest/maps/trek-places.client.ts @@ -12,9 +12,9 @@ * refused key falls back to the shared pool. The failure modes stay the network * and the service being down, both of which fall back to what TREK did before. */ -import { readEnv, getAppUrl } from '../../app-config'; -import { serviceIdentitySlot } from '../service-identity/service-identity.interface'; +import { readEnv } from '../../app-config'; import { toApiLang } from './maps.helpers'; +import { identifiedFetch } from './trek-places.fetch'; import { normalizePlaceWebsite, skiRouteBuildSchema, @@ -106,28 +106,6 @@ export interface TrekPlacesSearchResponse { results: TrekPlace[]; } -/** - * One opaque string per instance, so the service can rate-limit per caller - * instead of per IP. Derived from the instance URL rather than generated and - * stored: it must be stable across restarts, and it must not be anything that - * identifies a person. It is never resolved back to anything on the far side. - */ -function instanceToken(): string { - // getAppUrl(), not the raw APP_URL. The variable is commented out in the - // shipped compose file, so reading it raw gave every default install the same - // string — and a token every caller shares is not a caller. One install - // running a large import would then rate-limit all the others, who would fall - // back to Nominatim with nothing to point at. The resolver falls through to - // ALLOWED_ORIGINS and then to the port, so an unconfigured instance still - // differs from its neighbours. - const url = getAppUrl(); - let hash = 0; - for (let i = 0; i < url.length; i++) { - hash = ((hash << 5) - hash + url.charCodeAt(i)) | 0; - } - return `trek-${(hash >>> 0).toString(36)}`; -} - /** * Stop asking a service that is not answering. * @@ -255,26 +233,6 @@ function noteFailure(err: unknown, breaker: Breaker): void { } } -/** - * A request carrying who is asking: the instance token, and the installation - * key where it may go. A refused key costs no answer: the request goes once - * more without it, through the shared pool, while the identity counts the refusal. - */ -async function identifiedFetch(url: URL, headers: Record, signal: AbortSignal): Promise { - const identity = serviceIdentitySlot.get(); - const own = identity.headersFor(url); - const base = { ...headers, 'X-TREK-Instance': instanceToken() }; - const res = await fetch(url, { signal, headers: { ...base, ...own } }); - identity.observe(url, res); - if (res.status !== 401 || !own.Authorization) return res; - await res.body?.cancel(); - return fetch(url, { signal, headers: { ...base, ...withoutKey(own) } }); -} - -function withoutKey(headers: Record): Record { - return Object.fromEntries(Object.entries(headers).filter(([name]) => name !== 'Authorization')); -} - function statusError(status: number): Error & { status: number } { const err = new Error(`TREK Places API ${status}`) as Error & { status: number }; err.status = status; diff --git a/server/src/nest/maps/trek-places.fetch.ts b/server/src/nest/maps/trek-places.fetch.ts new file mode 100644 index 000000000..ec928fcdf --- /dev/null +++ b/server/src/nest/maps/trek-places.fetch.ts @@ -0,0 +1,48 @@ +import { getAppUrl } from '../../app-config'; +import { serviceIdentitySlot } from '../service-identity/service-identity.interface'; + +/** + * A request carrying who is asking: the instance token, and the installation + * key where it may go. A refused key costs no answer: the request goes once + * more without it, through the shared pool, while the identity counts the refusal. + */ +export async function identifiedFetch( + url: URL, + headers: Record, + signal: AbortSignal, +): Promise { + const identity = serviceIdentitySlot.get(); + const own = identity.headersFor(url); + const base = { ...headers, 'X-TREK-Instance': instanceToken() }; + const res = await fetch(url, { signal, headers: { ...base, ...own } }); + identity.observe(url, res); + if (res.status !== 401 || !own.Authorization) return res; + await res.body?.cancel(); + return fetch(url, { signal, headers: { ...base, ...withoutKey(own) } }); +} + +function withoutKey(headers: Record): Record { + return Object.fromEntries(Object.entries(headers).filter(([name]) => name !== 'Authorization')); +} + +/** + * One opaque string per instance, so the service can rate-limit per caller + * instead of per IP. Derived from the instance URL rather than generated and + * stored: it must be stable across restarts, and it must not be anything that + * identifies a person. It is never resolved back to anything on the far side. + */ +function instanceToken(): string { + // getAppUrl(), not the raw APP_URL. The variable is commented out in the + // shipped compose file, so reading it raw gave every default install the same + // string — and a token every caller shares is not a caller. One install + // running a large import would then rate-limit all the others, who would fall + // back to Nominatim with nothing to point at. The resolver falls through to + // ALLOWED_ORIGINS and then to the port, so an unconfigured instance still + // differs from its neighbours. + const url = getAppUrl(); + let hash = 0; + for (let i = 0; i < url.length; i++) { + hash = ((hash << 5) - hash + url.charCodeAt(i)) | 0; + } + return `trek-${(hash >>> 0).toString(36)}`; +}