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:
AetherForge
2026-06-04 20:41:44 -07:00
parent 6bfce5d5ab
commit 8466c7aa9b
101 changed files with 3369 additions and 1054 deletions

View File

@@ -2,7 +2,7 @@
* @vitest-environment happy-dom
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { act, render, renderHook } from '@testing-library/react';
import { act, render, renderHook, waitFor } from '@testing-library/react';
import { WebSocketProvider } from './WebSocketProvider';
import { useWebSocketContext } from './WebSocketContext';
import { useWebSocket } from '../hooks/useWebSocket';
@@ -50,6 +50,7 @@ describe('WebSocketProvider', () => {
MockWebSocket.instances = [];
setStoredAuth('testuser', 'testpass', { silent: true });
vi.stubGlobal('WebSocket', MockWebSocket as unknown as typeof WebSocket);
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('no ws ticket')));
Object.defineProperty(window, 'location', {
value: { protocol: 'http:', host: 'localhost:8080' },
configurable: true,
@@ -64,16 +65,23 @@ describe('WebSocketProvider', () => {
return MockWebSocket.instances.at(-1)!;
}
async function waitForSocket() {
await waitFor(() => {
expect(MockWebSocket.instances.length).toBeGreaterThan(0);
});
return latestSocket();
}
function wrapper({ children }: { children: React.ReactNode }) {
return <WebSocketProvider>{children}</WebSocketProvider>;
}
it('connects to ws dashboard with auth token query param', () => {
it('connects to ws dashboard with auth token query param', async () => {
setStoredAuth('drjones', 'secret');
MockWebSocket.instances = [];
const { result } = renderHook(() => useWebSocketContext(), { wrapper });
const ws = latestSocket();
const ws = await waitForSocket();
const token = btoa('drjones:secret');
expect(ws.url).toBe(`ws://localhost:8080/ws/dashboard?token=${encodeURIComponent(token)}`);
@@ -92,9 +100,10 @@ describe('WebSocketProvider', () => {
expect(useWebSocket).toBe(useWebSocketContext);
});
it('handles init and agent_online messages', () => {
it('handles init and agent_online messages', async () => {
const agent = mockAgent({ id: 'live-1' });
const { result } = renderHook(() => useWebSocketContext(), { wrapper });
await waitForSocket();
act(() => {
latestSocket().emitOpen();
@@ -115,9 +124,10 @@ describe('WebSocketProvider', () => {
expect(result.current.agents[0].hashrate_15s).toBe(999);
});
it('marks agent offline and caps recent shares', () => {
it('marks agent offline and caps recent shares', async () => {
const agent = mockAgent({ id: 'a-offline' });
const { result } = renderHook(() => useWebSocketContext(), { wrapper });
await waitForSocket();
act(() => {
latestSocket().emitOpen();
@@ -140,8 +150,9 @@ describe('WebSocketProvider', () => {
expect(result.current.recentShares.length).toBeLessThanOrEqual(50);
});
it('assigns monotonic _seq on command_result', () => {
it('assigns monotonic _seq on command_result', async () => {
const { result } = renderHook(() => useWebSocketContext(), { wrapper });
await waitForSocket();
act(() => {
latestSocket().emitOpen();
@@ -161,23 +172,24 @@ describe('WebSocketProvider', () => {
expect(result.current.agentLogs.a1).toBe('log data');
});
it('schedules reconnect after close', () => {
vi.useFakeTimers();
it('schedules reconnect after close', async () => {
MockWebSocket.instances = [];
renderHook(() => useWebSocketContext(), { wrapper });
const first = latestSocket();
const first = await waitForSocket();
act(() => first.close());
expect(MockWebSocket.instances).toHaveLength(1);
act(() => vi.advanceTimersByTime(3000));
expect(MockWebSocket.instances).toHaveLength(2);
vi.useRealTimers();
});
await act(async () => {
await new Promise((r) => setTimeout(r, 3100));
});
await waitFor(() => expect(MockWebSocket.instances).toHaveLength(2));
}, 10000);
it('closes socket on unmount', () => {
it('closes socket on unmount', async () => {
const closeSpy = vi.spyOn(MockWebSocket.prototype, 'close');
const { unmount } = render(<WebSocketProvider><span /></WebSocketProvider>);
await waitForSocket();
unmount();
expect(closeSpy).toHaveBeenCalled();
});

View File

@@ -9,7 +9,7 @@ import type {
import type { Agent, Share, FleetAlert, PoolStatus, AIActivityEntry, WSMessage } from '../types';
import { WebSocketContext } from './WebSocketContext';
import type { SeqCommandResult } from './WebSocketContext';
import { getStoredAuth } from '../api/auth';
import { authHeaders, getStoredAuth } from '../api/auth';
/**
* WebSocketProvider mounts a SINGLE WebSocket connection for the whole app.
@@ -54,25 +54,44 @@ export function WebSocketProvider({ children }: { children: React.ReactNode }) {
const existing = wsRef.current;
if (existing && (existing.readyState === WebSocket.OPEN || existing.readyState === WebSocket.CONNECTING)) {
existing.onclose = null;
existing.close();
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/dashboard?token=${encodeURIComponent(token)}`;
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
ws.onopen = () => { if (!unmounted.current) setIsConnected(true); };
ws.onclose = () => {
const openSocket = async () => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
let wsQuery = `token=${encodeURIComponent(token)}`;
try {
const resp = await fetch('/api/v1/auth/ws-ticket', {
method: 'POST',
headers: authHeaders(),
});
if (resp.ok) {
const data = (await resp.json()) as { ticket?: string };
if (data.ticket) {
wsQuery = `ticket=${encodeURIComponent(data.ticket)}`;
}
}
} catch {
/* fall back to legacy token query param */
}
if (unmounted.current) return;
setIsConnected(false);
if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
if (!getStoredAuth()) return;
reconnectTimer.current = setTimeout(connect, 3000);
};
ws.onerror = () => { ws.close(); };
const wsUrl = `${protocol}//${window.location.host}/ws/dashboard?${wsQuery}`;
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
ws.onopen = () => { if (!unmounted.current) setIsConnected(true); };
ws.onclose = () => {
if (unmounted.current) return;
setIsConnected(false);
if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
if (!getStoredAuth()) return;
reconnectTimer.current = setTimeout(connect, 3000);
};
ws.onerror = () => { ws.close(); };
ws.onmessage = (event) => {
try {
@@ -222,6 +241,9 @@ export function WebSocketProvider({ children }: { children: React.ReactNode }) {
console.error('Failed to parse WebSocket message:', err);
}
};
};
void openSocket();
}, []);
useEffect(() => {