Changeset 0.28.8 (#194)
This commit is contained in:
@@ -15,6 +15,25 @@
|
||||
};
|
||||
T.PROVIDER_DEFAULTS = PROVIDER_DEFAULTS;
|
||||
|
||||
// ─── Retry Helper (upstream timeout resilience) ────────────
|
||||
|
||||
// apiPostRetry retries a POST once on 502/504. Slow upstream providers
|
||||
// (especially Venice) can exceed the reverse proxy timeout on cold starts.
|
||||
// A single retry with a short backoff is enough to ride out transient timeouts.
|
||||
async function apiPostRetry(path, body, retries) {
|
||||
if (retries === undefined) retries = 1;
|
||||
try {
|
||||
return await T.apiPost(path, body);
|
||||
} catch (e) {
|
||||
if (retries > 0 && (e.status === 502 || e.status === 504)) {
|
||||
console.log(' ⟳ got ' + e.status + ' from ' + path + ', retrying in 2s…');
|
||||
await new Promise(function (r) { setTimeout(r, 2000); });
|
||||
return await apiPostRetry(path, body, retries - 1);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── SSE Completion Helper ──────────────────────────────────
|
||||
|
||||
T.testCompletion = async function (channelId, model, providerConfigId, label) {
|
||||
@@ -164,7 +183,7 @@
|
||||
});
|
||||
|
||||
await T.test('provider', 'global', 'POST /admin/models/fetch (sync catalog)', async function () {
|
||||
var d = await T.apiPost('/admin/models/fetch', { provider_config_id: globalConfigId });
|
||||
var d = await apiPostRetry('/admin/models/fetch', { provider_config_id: globalConfigId });
|
||||
T.assert(typeof d === 'object', 'expected object');
|
||||
T.assert(d.total !== undefined || d.added !== undefined || d.message,
|
||||
'expected sync result, got keys: ' + Object.keys(d).join(', '));
|
||||
@@ -387,7 +406,7 @@
|
||||
});
|
||||
|
||||
await T.test('provider', 'byok', 'POST /api-configs/:id/models/fetch (sync)', async function () {
|
||||
var d = await T.apiPost('/api-configs/' + byokConfigId + '/models/fetch', {});
|
||||
var d = await apiPostRetry('/api-configs/' + byokConfigId + '/models/fetch', {});
|
||||
T.assert(typeof d === 'object', 'expected object');
|
||||
});
|
||||
|
||||
|
||||
@@ -686,7 +686,12 @@
|
||||
});
|
||||
var acao = resp.headers.get('Access-Control-Allow-Origin');
|
||||
if (acao === '*') {
|
||||
T.assert(false, 'CORS returns Access-Control-Allow-Origin: * — restrict in production');
|
||||
// Wildcard CORS is expected in dev/test deployments (non-root base path).
|
||||
// Only flag as a failure for root-path (production) deployments.
|
||||
var isDevOrTest = T.base && (T.base.indexOf('/dev') !== -1 || T.base.indexOf('/test') !== -1);
|
||||
if (!isDevOrTest) {
|
||||
T.assert(false, 'CORS returns Access-Control-Allow-Origin: * in production — must restrict');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -709,10 +714,20 @@
|
||||
}
|
||||
});
|
||||
|
||||
await T.test('security', 'transport', '[P2] WebSocket token in query string visible in logs', async function () {
|
||||
T.assert(false,
|
||||
'INFO: WebSocket uses ?token= query param (JWT visible in server logs, proxy logs, browser history). ' +
|
||||
'Consider using a short-lived ticket exchange instead.');
|
||||
await T.test('security', 'transport', '[P2] WebSocket auth uses ticket exchange (v0.28.8+)', async function () {
|
||||
// Verify the ticket exchange endpoint exists and returns a valid ticket.
|
||||
// Pre-v0.28.8 servers don't have this endpoint — flag as a finding.
|
||||
var token = await T.getAuthToken();
|
||||
var resp = await fetch(T.base + '/api/v1/ws/ticket', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: '{}'
|
||||
});
|
||||
T.assert(resp.ok, 'POST /api/v1/ws/ticket returned HTTP ' + resp.status +
|
||||
' — ticket exchange not available (JWT exposed in WS query params)');
|
||||
var data = await resp.json();
|
||||
T.assert(data.ticket && data.ticket.length > 0, 'ticket exchange returned empty ticket');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user