refactor(server): share one proxy-header check across the local gates

localRequestGate.mjs owns the reverse-proxy header list and hasProxySignals;
Provider Settings, the cross-site gate and /mcp all use it instead of two
copies of the same nine names. The changelog credits the cross-site gate.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Sameh KhamisandClaude Opus 5.5 committed 2026-10-02 20:02:48 -07:00
1 parent bbf4976f5e
commit 6cdd6c8af9
5 files changed
+34 -34

No files matched your search

+7
View File
@@ -2,6 +2,13 @@
## [Unreleased] ## [Unreleased]
- Refuse cross-site browser requests to the cost-bearing and log endpoints
(`/api/realtime/token`, `/api/openai/hud-summary`,
`/api/google/nearby-places`, `/api/realtime/debug-log`): a foreign or opaque
Origin, a cross-site `Sec-Fetch-Site`, or proxy forwarding headers get 403,
while loopback tools and LAN use keep working. The dev and preview servers
send one shared Content-Security-Policy (James Sumpter, #242).
- The voice debug log records the server's own time: a record can no longer - The voice debug log records the server's own time: a record can no longer
supply its `loggedAt`. The dev and preview servers also send supply its `loggedAt`. The dev and preview servers also send
`X-Content-Type-Options: nosniff` (findings by Sunil, #710). `X-Content-Type-Options: nosniff` (findings by Sunil, #710).
+7 -1
View File
@@ -306,6 +306,7 @@
"server/providers/common/source-root.js", "server/providers/common/source-root.js",
"server/standalone/key-setup.js", "server/standalone/key-setup.js",
"src/keySetupCore.mjs", "src/keySetupCore.mjs",
"src/localRequestGate.mjs",
"server/standalone/key-setup-hardening.mjs" "server/standalone/key-setup-hardening.mjs"
], ],
"external": [] "external": []
@@ -390,7 +391,8 @@
"src/ui/mapSource.js", "src/ui/mapSource.js",
"src/ui/mapSourceControls.js", "src/ui/mapSourceControls.js",
"src/mapStackChips.js", "src/mapStackChips.js",
"src/keySetupCore.mjs" "src/keySetupCore.mjs",
"src/localRequestGate.mjs"
], ],
"external": [] "external": []
}, },
@@ -409,6 +411,7 @@
"src/loadingFeedback.js", "src/loadingFeedback.js",
"src/data/installationFeedback.js", "src/data/installationFeedback.js",
"src/keySetupCore.mjs", "src/keySetupCore.mjs",
"src/localRequestGate.mjs",
"src/data/feedState.js", "src/data/feedState.js",
"src/data/layerSnapshot.js" "src/data/layerSnapshot.js"
], ],
@@ -529,6 +532,7 @@
"src/data/militaryAwarenessEngine.js", "src/data/militaryAwarenessEngine.js",
"src/data/scenePick.js", "src/data/scenePick.js",
"src/keySetupCore.mjs", "src/keySetupCore.mjs",
"src/localRequestGate.mjs",
"src/loadingFeedback.js", "src/loadingFeedback.js",
"src/locationStatus.js", "src/locationStatus.js",
"src/mapStackChips.js", "src/mapStackChips.js",
@@ -1266,6 +1270,7 @@
"src/maps/terrain.js", "src/maps/terrain.js",
"src/maps/terrainRetry.js", "src/maps/terrainRetry.js",
"src/keySetupCore.mjs", "src/keySetupCore.mjs",
"src/localRequestGate.mjs",
"src/maps/imageryHost.js" "src/maps/imageryHost.js"
], ],
"external": ["cesium"] "external": ["cesium"]
@@ -1511,6 +1516,7 @@
"src/hudSummaryResponse.js", "src/hudSummaryResponse.js",
"src/keySetup.js", "src/keySetup.js",
"src/keySetupCore.mjs", "src/keySetupCore.mjs",
"src/localRequestGate.mjs",
"src/keylessGeocoder.js", "src/keylessGeocoder.js",
"src/layers/aircraft/classification.js", "src/layers/aircraft/classification.js",
"src/layers/alpr/index.js", "src/layers/alpr/index.js",
+2 -1
View File
@@ -7,7 +7,8 @@
* rebinding) and, when a browser sends an Origin, come from that same host. * rebinding) and, when a browser sends an Origin, come from that same host.
*/ */
import { hasProxySignals, isSharingEnabled } from '../../src/keySetupCore.mjs'; import { isSharingEnabled } from '../../src/keySetupCore.mjs';
import { hasProxySignals } from '../../src/localRequestGate.mjs';
import { createMcpHttpHandler } from '../../src/tools/mcp/index.js'; import { createMcpHttpHandler } from '../../src/tools/mcp/index.js';
import { createLocalMcpServer } from './server.js'; import { createLocalMcpServer } from './server.js';
+4 -25
View File
@@ -1,7 +1,8 @@
/** /**
* Key setup ("POWER UP") — the pure core. * Key setup ("POWER UP") — the pure core.
* *
* One registry, three pure functions, zero dependencies. The dev server's * One registry and pure functions; its only import is the proxy-header check
* shared with the other local gates (localRequestGate.mjs). The dev server's
* /api/setup endpoints (vite.config.js) and the in-app panel (keySetup.js) * /api/setup endpoints (vite.config.js) and the in-app panel (keySetup.js)
* are both thin shells over this module, so what a key is called, what it * are both thin shells over this module, so what a key is called, what it
* unlocks, and how a .env line is written each live in exactly one place. * unlocks, and how a .env line is written each live in exactly one place.
@@ -11,6 +12,8 @@
* behavior below unit-testable. * behavior below unit-testable.
*/ */
import { hasProxySignals } from './localRequestGate.mjs';
/** Longest accepted key/token value. Real provider keys are all far shorter. */ /** Longest accepted key/token value. Real provider keys are all far shorter. */
export const KEY_SETUP_VALUE_LIMIT = 512; export const KEY_SETUP_VALUE_LIMIT = 512;
@@ -184,30 +187,6 @@ export function parseWindowsUserSid(stdout) {
: null; : null;
} }
/** Headers that reverse proxies, tunnels and CDNs add to a request. */
export const PROXY_SIGNAL_HEADERS = Object.freeze([
'forwarded',
'via',
'x-forwarded-for',
'x-forwarded-host',
'x-forwarded-port',
'x-forwarded-proto',
'x-real-ip',
'cf-connecting-ip',
'cf-ray',
]);
/**
* Whether a request carries reverse-proxy or CDN forwarding headers, so it
* did not originate on this machine whatever its socket says. `headers` is
* keyed by lower-case header name.
*/
export function hasProxySignals(headers = {}) {
return PROXY_SIGNAL_HEADERS.some(
(name) => String(headers[name] || '').trim() !== '',
);
}
/** /**
* Whether the launcher has sharing on. Every sharing signal the launcher * Whether the launcher has sharing on. Every sharing signal the launcher
* recognizes (scripts/pinokio-preflight.mjs) counts, so the two sets cannot * recognizes (scripts/pinokio-preflight.mjs) counts, so the two sets cannot
+14 -7
View File
@@ -21,10 +21,9 @@
*/ */
/** /**
* Reverse-proxy / CDN forwarding headers. Their presence means the request did * Reverse-proxy / CDN forwarding headers. Their presence means the request did
* not originate on this machine, whatever its socket says. Kept as this * not originate on this machine, whatever its socket says. This module owns
* module's own list (keySetupCore.mjs carries the same nine names for the * the list for every local gate (the credential panel and /mcp import
* credential panel) so the gate has no imports and can sit in any package * `hasProxySignals`), and has no imports so it can sit in any package boundary.
* boundary.
*/ */
export const PROXY_SIGNALS = Object.freeze([ export const PROXY_SIGNALS = Object.freeze([
'forwarded', 'forwarded',
@@ -38,6 +37,16 @@ export const PROXY_SIGNALS = Object.freeze([
'cf-ray', 'cf-ray',
]); ]);
/**
* Whether a request carries reverse-proxy or CDN forwarding headers. `headers`
* is keyed by lower-case header name.
*/
export function hasProxySignals(headers = {}) {
return PROXY_SIGNALS.some(
(name) => String(headers[name] || '').trim() !== '',
);
}
/** /**
* Compute a request's own authority (an origin string) from its protocol and * Compute a request's own authority (an origin string) from its protocol and
* Host header. Unlike the credential-panel gate's localhost-restricted * Host header. Unlike the credential-panel gate's localhost-restricted
@@ -90,9 +99,7 @@ export function admitSameSiteRequest({
} = {}) { } = {}) {
// (1) A request carrying reverse-proxy / CDN forwarding headers did not // (1) A request carrying reverse-proxy / CDN forwarding headers did not
// originate on this machine, whatever its socket says. // originate on this machine, whatever its socket says.
if ( if (hasProxySignals(proxyHeaders)) {
PROXY_SIGNALS.some((name) => String(proxyHeaders[name] || '').trim() !== '')
) {
return { return {
ok: false, ok: false,
status: 403, status: 403,