mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +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.
|
||||
|
||||
- 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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
- 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).
|
||||
- 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`.
|
||||
- `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.
|
||||
|
||||
@@ -132,19 +132,25 @@ function createRealtimeTokenHandler({
|
||||
});
|
||||
const body = await response.text();
|
||||
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
|
||||
// body is passed through untouched (the client parses it verbatim), so
|
||||
// these headers are the authoritative echo — including the case where a
|
||||
// bogus ?tier= was silently downgraded to standard.
|
||||
// success body is passed through untouched (the client parses it
|
||||
// verbatim), so these headers are the authoritative echo — including the
|
||||
// case where a bogus ?tier= was silently downgraded to standard.
|
||||
res.setHeader('X-GEV-Voice-Tier', tier);
|
||||
res.setHeader('X-GEV-Voice-Model', model);
|
||||
if (requestedTier && !isKnownVoiceTier(requestedTier)) {
|
||||
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);
|
||||
} catch {
|
||||
// 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',
|
||||
});
|
||||
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['cache-control'], 'no-store');
|
||||
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('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.
|
||||
t.mock.restoreAll();
|
||||
t.mock.method(globalThis, 'fetch', async () => {
|
||||
|
||||
Reference in new issue
Block a user