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.
This commit is contained in:
Maurice
2026-10-29 09:42:17 +01:00
committed by Maurice
parent eeae904867
commit cb546e4a72
2 changed files with 50 additions and 44 deletions
+2 -44
View File
@@ -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<string, string>, signal: AbortSignal): Promise<Response> {
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<string, string>): Record<string, string> {
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;
+48
View File
@@ -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<string, string>,
signal: AbortSignal,
): Promise<Response> {
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<string, string>): Record<string, string> {
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)}`;
}