Files
openchamber/packages/web/bin/lib/cli-http.js
T
Bohdan Triapitsyn 5cc37d0c33 fix: restore CLI validation and test baseline (#1857)
Fixed lazy CLI helper imports for tunnel flows
Restored update command version detection
Made web test and dead-code commands run reliably
2026-06-27 09:45:34 +03:00

217 lines
6.6 KiB
JavaScript

import { buildLocalUrl } from './cli-network.js';
import { getInstanceFilePath, readInstanceOptions } from './cli-process.js';
const UI_SESSION_COOKIE_NAME = 'oc_ui_session';
function extractUiSessionCookie(response) {
const setCookie = response?.headers?.get?.('set-cookie');
if (typeof setCookie !== 'string' || setCookie.length === 0) {
return null;
}
const match = setCookie.match(new RegExp(`(?:^|,\\s*)(${UI_SESSION_COOKIE_NAME}=[^;]+)`));
return match?.[1] || null;
}
async function resolveUiPasswordForPort(port, options = {}) {
if (typeof options.uiPassword === 'string' && options.uiPassword.trim().length > 0) {
return options.uiPassword;
}
const instanceOptions = readInstanceOptions(await getInstanceFilePath(port));
return typeof instanceOptions?.uiPassword === 'string' && instanceOptions.uiPassword.trim().length > 0
? instanceOptions.uiPassword
: null;
}
async function createUiSessionCookie(port, password, timeoutMs) {
if (typeof password !== 'string' || password.length === 0) {
return null;
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(buildLocalUrl(port, '/auth/session'), {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ password }),
signal: controller.signal,
});
if (!response.ok) {
return null;
}
return extractUiSessionCookie(response);
} catch {
return null;
} finally {
clearTimeout(timeout);
}
}
async function requestServerShutdown(port, hostOverride) {
if (!Number.isFinite(port) || port <= 0) return false;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 1500);
try {
const resp = await fetch(buildLocalUrl(port, '/api/system/shutdown', hostOverride), {
method: 'POST',
signal: controller.signal,
});
return resp.ok;
} catch {
return false;
} finally {
clearTimeout(timeout);
}
}
async function requestJson(port, endpoint, options = {}) {
const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
? Math.trunc(options.timeoutMs)
: 4000;
const fetchOptions = { ...options };
delete fetchOptions.timeoutMs;
delete fetchOptions.uiPassword;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const requestUrl = buildLocalUrl(port, endpoint);
const requestHeaders = {
Accept: 'application/json',
...(fetchOptions.body ? { 'Content-Type': 'application/json' } : {}),
...(fetchOptions.headers || {}),
};
const response = await fetch(requestUrl, {
...fetchOptions,
headers: requestHeaders,
signal: controller.signal,
});
const body = await response.json().catch(() => null);
if (response.status === 401 && body?.error === 'UI authentication required') {
const uiPassword = await resolveUiPasswordForPort(port, options);
const cookie = await createUiSessionCookie(port, uiPassword, timeoutMs);
if (cookie) {
const retryResponse = await fetch(requestUrl, {
...fetchOptions,
headers: {
...requestHeaders,
Cookie: cookie,
},
signal: controller.signal,
});
const retryBody = await retryResponse.json().catch(() => null);
return { response: retryResponse, body: retryBody };
}
}
return { response, body };
} catch (error) {
if (error && (error.name === 'AbortError' || error.code === 'ABORT_ERR')) {
throw new Error(`Request to ${endpoint} timed out after ${timeoutMs}ms.`);
}
throw error;
} finally {
clearTimeout(timeout);
}
}
async function isServerHealthReady(port, timeoutMs = 1000) {
if (!Number.isFinite(port) || port <= 0) {
return false;
}
const requestTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.trunc(timeoutMs) : 1000;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), requestTimeout);
try {
const response = await fetch(buildLocalUrl(port, '/health'), {
headers: { Accept: 'text/plain' },
signal: controller.signal,
});
return response.ok;
} catch {
return false;
} finally {
clearTimeout(timeout);
}
}
async function waitForServerHealth(port, {
timeoutMs = 60000,
intervalMs = 250,
onTick,
} = {}) {
const start = Date.now();
const deadline = start + timeoutMs;
while (Date.now() < deadline) {
const elapsedMs = Date.now() - start;
if (typeof onTick === 'function') {
onTick({ elapsedMs, timeoutMs });
}
if (await isServerHealthReady(port, Math.min(1000, intervalMs * 2))) {
if (typeof onTick === 'function') {
onTick({ elapsedMs: Math.min(Date.now() - start, timeoutMs), timeoutMs, complete: true });
}
return true;
}
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
if (typeof onTick === 'function') {
onTick({ elapsedMs: timeoutMs, timeoutMs, timedOut: true });
}
return false;
}
async function fetchTunnelProvidersFromPort(port, fetchImpl = globalThis.fetch) {
if (!Number.isFinite(port) || port <= 0 || typeof fetchImpl !== 'function') {
return null;
}
try {
const response = await fetchImpl(buildLocalUrl(port, '/api/openchamber/tunnel/providers'));
if (!response.ok) return null;
const body = await response.json().catch(() => null);
if (!body || !Array.isArray(body.providers)) return null;
return body.providers;
} catch {
return null;
}
}
async function fetchSystemInfoFromPort(port, fetchImpl = globalThis.fetch, hostOverride) {
if (!Number.isFinite(port) || port <= 0 || typeof fetchImpl !== 'function') {
return null;
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 1500);
try {
const response = await fetchImpl(buildLocalUrl(port, '/api/system/info', hostOverride), {
headers: { Accept: 'application/json' },
signal: controller.signal,
});
if (!response.ok) return null;
const body = await response.json().catch(() => null);
if (!body || typeof body.runtime !== 'string') return null;
return {
runtime: body.runtime,
pid: Number.isFinite(body.pid) ? body.pid : null,
};
} catch {
return null;
} finally {
clearTimeout(timeout);
}
}
export {
requestServerShutdown,
requestJson,
isServerHealthReady,
waitForServerHealth,
fetchTunnelProvidersFromPort,
fetchSystemInfoFromPort,
};