mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
fix: sanitize rejected Realtime token responses
This commit is contained in:
1 parent
a88904a02f
commit
2608f187d3
4 files changed
+36
-9
No files matched your search
+1
-1
@@ -133,7 +133,7 @@
|
|||||||
|
|
||||||
- Expose portable radio, camera-type and regional source helpers; keep HTTP transport separate from record normalization.
|
- Expose portable radio, camera-type and regional source helpers; keep HTTP transport separate from record normalization.
|
||||||
|
|
||||||
- Three proxy paths no longer relay upstream or JS error text to the client. The HUD summary passed OpenAI's own `error.message` through whenever upstream was not ok, carrying request ids and quota wording; the Realtime token route echoed the JS error, which for a network fault names the upstream host; and a failed CCTV media fetch stored the raw errno as the camera's health message, which reaches the screen through `GET /api/cctv/health` rather than through the sanitized response beside it. Logs now name the failure and the upstream status without the text.
|
- Three proxy paths no longer relay upstream or JS error text to the client. The HUD summary passed OpenAI's own `error.message` through whenever upstream was not ok, carrying request ids and quota wording; the Realtime token route passed through non-success response bodies and echoed JS errors, which can expose upstream details; and a failed CCTV media fetch stored the raw errno as the camera's health message, which reaches the screen through `GET /api/cctv/health` rather than through the sanitized response beside it. Logs now name the failure and the upstream status without the text.
|
||||||
|
|
||||||
- Separate vessel records and feed acquisition from rendering while preserving selection, partial-feed retention, sea-surface placement and request cancellation.
|
- Separate vessel records and feed acquisition from rendering while preserving selection, partial-feed retention, sea-surface placement and request cancellation.
|
||||||
|
|
||||||
|
|||||||
@@ -3573,7 +3573,7 @@ are omitted rather than framing the wrong part of the globe.
|
|||||||
- A cold OpenSky failure uses the current camera subpoint only to request a cached adsb.lol point fallback capped at 250 nm. A fresh OpenSky response or last-good cache wins; a nominally successful worldwide snapshot more than two minutes old prefers viewport-scoped adsb.lol when available, otherwise the stale source is reported honestly. The fallback is visibly source-labeled and is never presented as a worldwide snapshot.
|
- A cold OpenSky failure uses the current camera subpoint only to request a cached adsb.lol point fallback capped at 250 nm. A fresh OpenSky response or last-good cache wins; a nominally successful worldwide snapshot more than two minutes old prefers viewport-scoped adsb.lol when available, otherwise the stale source is reported honestly. The fallback is visibly source-labeled and is never presented as a worldwide snapshot.
|
||||||
- GBFS proxy refuses upstream redirects (`redirect: 'manual'`; any 3xx becomes a 502 and the redirect target is logged server-side only) and enforces its 5 MB response cap while the body streams, cancelling the upstream read past the cap; CCTV health map is bounded.
|
- GBFS proxy refuses upstream redirects (`redirect: 'manual'`; any 3xx becomes a 502 and the redirect target is logged server-side only) and enforces its 5 MB response cap while the body streams, cancelling the upstream read past the cap; CCTV health map is bounded.
|
||||||
- Proxy error payloads are sanitized (no internal error details returned to clients).
|
- Proxy error payloads are sanitized (no internal error details returned to clients).
|
||||||
- That holds for the OpenAI and CCTV media paths too: `/api/openai/hud-summary` never relays OpenAI's own `error.message`, `/api/realtime/token` answers a fixed string when minting fails, and a failed CCTV media fetch stores a fixed camera health `message` — `GET /api/cctv/health` serializes that field and the CCTV panel renders it as a status label, so it is a client surface as much as the response body is.
|
- That holds for the OpenAI and CCTV media paths too: `/api/openai/hud-summary` never relays OpenAI's own `error.message`, `/api/realtime/token` passes successful ephemeral-token responses through but answers with a fixed error when minting fails or upstream rejects the request, and a failed CCTV media fetch stores a fixed camera health `message` — `GET /api/cctv/health` serializes that field and the CCTV panel renders it as a status label, so it is a client surface as much as the response body is.
|
||||||
- `OPENAI_API_KEY` is server-side only; the browser receives ephemeral Realtime client secrets from `/api/realtime/token`.
|
- `OPENAI_API_KEY` is server-side only; the browser receives ephemeral Realtime client secrets from `/api/realtime/token`.
|
||||||
- `AISSTREAM_API_KEY` is server-side only; the browser reads the same-origin `/api/ais-live` cache.
|
- `AISSTREAM_API_KEY` is server-side only; the browser reads the same-origin `/api/ais-live` cache.
|
||||||
- `/api/google/nearby-places` keeps the Google key out of Places requests issued for voice scene context.
|
- `/api/google/nearby-places` keeps the Google key out of Places requests issued for voice scene context.
|
||||||
|
|||||||
@@ -132,19 +132,25 @@ function createRealtimeTokenHandler({
|
|||||||
});
|
});
|
||||||
const body = await response.text();
|
const body = await response.text();
|
||||||
res.statusCode = response.status;
|
res.statusCode = response.status;
|
||||||
res.setHeader(
|
|
||||||
'Content-Type',
|
|
||||||
response.headers.get('content-type') || 'application/json',
|
|
||||||
);
|
|
||||||
// Which tier/model this secret was actually minted for. The upstream
|
// Which tier/model this secret was actually minted for. The upstream
|
||||||
// body is passed through untouched (the client parses it verbatim), so
|
// success body is passed through untouched (the client parses it
|
||||||
// these headers are the authoritative echo — including the case where a
|
// verbatim), so these headers are the authoritative echo — including the
|
||||||
// bogus ?tier= was silently downgraded to standard.
|
// case where a bogus ?tier= was silently downgraded to standard.
|
||||||
res.setHeader('X-GEV-Voice-Tier', tier);
|
res.setHeader('X-GEV-Voice-Tier', tier);
|
||||||
res.setHeader('X-GEV-Voice-Model', model);
|
res.setHeader('X-GEV-Voice-Model', model);
|
||||||
if (requestedTier && !isKnownVoiceTier(requestedTier)) {
|
if (requestedTier && !isKnownVoiceTier(requestedTier)) {
|
||||||
res.setHeader('X-GEV-Voice-Tier-Fallback', '1');
|
res.setHeader('X-GEV-Voice-Tier-Fallback', '1');
|
||||||
}
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
console.warn(`[realtime-token] upstream HTTP ${response.status}`);
|
||||||
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
res.end(JSON.stringify({ error: 'Failed to create Realtime token' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.setHeader(
|
||||||
|
'Content-Type',
|
||||||
|
response.headers.get('content-type') || 'application/json',
|
||||||
|
);
|
||||||
res.end(body);
|
res.end(body);
|
||||||
} catch {
|
} catch {
|
||||||
// For a network fault this was a resolver message naming the upstream
|
// For a network fault this was a resolver message naming the upstream
|
||||||
|
|||||||
@@ -279,6 +279,7 @@ test('Realtime service configuration selects compatible endpoint/model without f
|
|||||||
url: '/?tier=arbitrary-model&model=other',
|
url: '/?tier=arbitrary-model&model=other',
|
||||||
});
|
});
|
||||||
assert.equal(response.status, 200);
|
assert.equal(response.status, 200);
|
||||||
|
assert.deepEqual(response.json(), { value: 'short-lived-fixture' });
|
||||||
assert.equal(response.headers['x-gev-voice-model'], 'configured-model');
|
assert.equal(response.headers['x-gev-voice-model'], 'configured-model');
|
||||||
assert.equal(response.headers['cache-control'], 'no-store');
|
assert.equal(response.headers['cache-control'], 'no-store');
|
||||||
assert.doesNotMatch(response.body, /server-fixture|voice\.example/);
|
assert.doesNotMatch(response.body, /server-fixture|voice\.example/);
|
||||||
@@ -303,6 +304,26 @@ test('OpenAI routes answer generically when the upstream or the request fails',
|
|||||||
assert.equal(summary.body.includes('req_fixture_1234'), false);
|
assert.equal(summary.body.includes('req_fixture_1234'), false);
|
||||||
assert.equal(summary.body.includes('fixture-upstream-secret'), false);
|
assert.equal(summary.body.includes('fixture-upstream-secret'), false);
|
||||||
|
|
||||||
|
// An HTTP error from the client-secret endpoint carries the same upstream
|
||||||
|
// detail, while a successful response must still pass the ephemeral secret.
|
||||||
|
t.mock.restoreAll();
|
||||||
|
t.mock.method(globalThis, 'fetch', async () =>
|
||||||
|
Response.json({ error: { message: leak } }, { status: 429 }),
|
||||||
|
);
|
||||||
|
const rejectedToken = await request(
|
||||||
|
install(openAiRealtimeProxy()).get('/api/realtime/token'),
|
||||||
|
);
|
||||||
|
assert.equal(rejectedToken.status, 429);
|
||||||
|
assert.equal(
|
||||||
|
rejectedToken.headers['content-type'],
|
||||||
|
'application/json; charset=utf-8',
|
||||||
|
);
|
||||||
|
assert.deepEqual(rejectedToken.json(), {
|
||||||
|
error: 'Failed to create Realtime token',
|
||||||
|
});
|
||||||
|
assert.equal(rejectedToken.body.includes('req_fixture_1234'), false);
|
||||||
|
assert.equal(rejectedToken.body.includes('fixture-upstream-secret'), false);
|
||||||
|
|
||||||
// A network fault surfaced a resolver message naming the upstream host.
|
// A network fault surfaced a resolver message naming the upstream host.
|
||||||
t.mock.restoreAll();
|
t.mock.restoreAll();
|
||||||
t.mock.method(globalThis, 'fetch', async () => {
|
t.mock.method(globalThis, 'fetch', async () => {
|
||||||
|
|||||||
Reference in new issue
Block a user