Files
worldmonitor/src/services/runtime.ts
Elie Habib d0a2a50506 fix(desktop): backoff on errors to stop CPU abuse + shrink settings window (#633)
Three bugs combine to burn 130% CPU when sidecar auth fails:

1. RefreshScheduler resets backoff multiplier to 1 (fastest) on error,
   causing failed endpoints to poll at base interval instead of backing off.
   Fix: exponential backoff on errors, same as unchanged-data path.

2. classify-event batch system ignores 401 (auth failure) — only pauses
   on 429/5xx. Hundreds of classify calls fire every 2s, each wasted.
   Fix: pause 120s on 401, matching the 429/5xx pattern.

3. Fetch patch retries every 401 (refresh token + retry), doubling all
   requests to the sidecar even when token refresh consistently fails.
   Fix: 60s cooldown after a retry-401 still returns 401.

Also shrinks settings window from 760→600px (min 620→480) to reduce
the empty whitespace below content on all tabs.
2026-03-01 10:53:54 +04:00

425 lines
14 KiB
TypeScript

const WS_API_URL = import.meta.env.VITE_WS_API_URL || '';
const DEFAULT_REMOTE_HOSTS: Record<string, string> = {
tech: WS_API_URL,
full: WS_API_URL,
world: WS_API_URL,
happy: WS_API_URL,
};
const DEFAULT_LOCAL_API_PORT = 46123;
const FORCE_DESKTOP_RUNTIME = import.meta.env.VITE_DESKTOP_RUNTIME === '1';
let _resolvedPort: number | null = null;
let _portPromise: Promise<number> | null = null;
export async function resolveLocalApiPort(): Promise<number> {
if (_resolvedPort !== null) return _resolvedPort;
if (_portPromise) return _portPromise;
_portPromise = (async () => {
try {
const { tryInvokeTauri } = await import('@/services/tauri-bridge');
const port = await tryInvokeTauri<number>('get_local_api_port');
if (port && port > 0) {
_resolvedPort = port;
return port;
}
} catch {
// IPC failed — allow retry on next call
} finally {
_portPromise = null;
}
return DEFAULT_LOCAL_API_PORT;
})();
return _portPromise;
}
export function getLocalApiPort(): number {
return _resolvedPort ?? DEFAULT_LOCAL_API_PORT;
}
function normalizeBaseUrl(baseUrl: string): string {
return baseUrl.replace(/\/$/, '');
}
type RuntimeProbe = {
hasTauriGlobals: boolean;
userAgent: string;
locationProtocol: string;
locationHost: string;
locationOrigin: string;
};
export function detectDesktopRuntime(probe: RuntimeProbe): boolean {
const tauriInUserAgent = probe.userAgent.includes('Tauri');
const secureLocalhostOrigin = (
probe.locationProtocol === 'https:' && (
probe.locationHost === 'localhost' ||
probe.locationHost.startsWith('localhost:') ||
probe.locationHost === '127.0.0.1' ||
probe.locationHost.startsWith('127.0.0.1:')
)
);
// Tauri production windows can expose tauri-like hosts/schemes without
// always exposing bridge globals at first paint.
const tauriLikeLocation = (
probe.locationProtocol === 'tauri:' ||
probe.locationProtocol === 'asset:' ||
probe.locationHost === 'tauri.localhost' ||
probe.locationHost.endsWith('.tauri.localhost') ||
probe.locationOrigin.startsWith('tauri://') ||
secureLocalhostOrigin
);
return probe.hasTauriGlobals || tauriInUserAgent || tauriLikeLocation;
}
export function isDesktopRuntime(): boolean {
if (FORCE_DESKTOP_RUNTIME) {
return true;
}
if (typeof window === 'undefined') {
return false;
}
return detectDesktopRuntime({
hasTauriGlobals: '__TAURI_INTERNALS__' in window || '__TAURI__' in window,
userAgent: window.navigator?.userAgent ?? '',
locationProtocol: window.location?.protocol ?? '',
locationHost: window.location?.host ?? '',
locationOrigin: window.location?.origin ?? '',
});
}
export function getApiBaseUrl(): string {
if (!isDesktopRuntime()) {
return '';
}
const configuredBaseUrl = import.meta.env.VITE_TAURI_API_BASE_URL;
if (configuredBaseUrl) {
return normalizeBaseUrl(configuredBaseUrl);
}
return `http://127.0.0.1:${getLocalApiPort()}`;
}
export function getRemoteApiBaseUrl(): string {
const configuredRemoteBase = import.meta.env.VITE_TAURI_REMOTE_API_BASE_URL;
if (configuredRemoteBase) {
return normalizeBaseUrl(configuredRemoteBase);
}
const variant = import.meta.env.VITE_VARIANT || 'full';
return DEFAULT_REMOTE_HOSTS[variant] ?? DEFAULT_REMOTE_HOSTS.full ?? '';
}
export function toRuntimeUrl(path: string): string {
if (!path.startsWith('/')) {
return path;
}
const baseUrl = getApiBaseUrl();
if (!baseUrl) {
return path;
}
return `${baseUrl}${path}`;
}
function extractHostnames(...urls: (string | undefined)[]): string[] {
const hosts: string[] = [];
for (const u of urls) {
if (!u) continue;
try { hosts.push(new URL(u).hostname); } catch {}
}
return hosts;
}
const APP_HOSTS = new Set([
'worldmonitor.app',
'www.worldmonitor.app',
'tech.worldmonitor.app',
'api.worldmonitor.app',
'localhost',
'127.0.0.1',
...extractHostnames(WS_API_URL, import.meta.env.VITE_WS_RELAY_URL),
]);
function isAppOriginUrl(urlStr: string): boolean {
try {
const u = new URL(urlStr);
const host = u.hostname;
return APP_HOSTS.has(host) || host.endsWith('.worldmonitor.app');
} catch {
return false;
}
}
function getApiTargetFromRequestInput(input: RequestInfo | URL): string | null {
if (typeof input === 'string') {
if (input.startsWith('/')) return input;
if (isAppOriginUrl(input)) {
const u = new URL(input);
return `${u.pathname}${u.search}`;
}
return null;
}
if (input instanceof URL) {
if (isAppOriginUrl(input.href)) {
return `${input.pathname}${input.search}`;
}
return null;
}
if (isAppOriginUrl(input.url)) {
const u = new URL(input.url);
return `${u.pathname}${u.search}`;
}
return null;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isLocalOnlyApiTarget(target: string): boolean {
// Security boundary: endpoints that can carry local secrets must use the
// `/api/local-*` prefix so cloud fallback is automatically blocked.
return target.startsWith('/api/local-');
}
function isKeyFreeApiTarget(target: string): boolean {
return target.startsWith('/api/register-interest');
}
async function fetchLocalWithStartupRetry(
nativeFetch: typeof window.fetch,
localUrl: string,
init?: RequestInit,
): Promise<Response> {
const maxAttempts = 4;
let lastError: unknown = null;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
try {
return await nativeFetch(localUrl, init);
} catch (error) {
lastError = error;
// Preserve caller intent for aborted requests.
if (init?.signal?.aborted) {
throw error;
}
if (attempt === maxAttempts) {
break;
}
await sleep(125 * attempt);
}
}
throw lastError instanceof Error
? lastError
: new Error('Local API unavailable');
}
// ── Security threat model for the fetch patch ──────────────────────────
// The LOCAL_API_TOKEN exists to prevent OTHER local processes from
// accessing the sidecar on port 46123. The renderer IS the intended
// client — injecting the token automatically is correct by design.
//
// If the renderer is compromised (XSS, supply chain), the attacker
// already has access to strictly more powerful Tauri IPC commands
// (get_all_secrets, set_secret, etc.) via window.__TAURI_INTERNALS__.
// The fetch patch does not expand the attack surface beyond what IPC
// already provides.
//
// Defense layers that protect the renderer trust boundary:
// 1. CSP: script-src 'self' (no unsafe-inline/eval)
// 2. IPC origin validation: sensitive commands gated to trusted windows
// 3. Sidecar allowlists: env-update restricted to ALLOWED_ENV_KEYS
// 4. DevTools disabled in production builds
//
// The token has a 5-minute TTL in the closure to limit exposure window
// if IPC access is revoked mid-session.
const TOKEN_TTL_MS = 5 * 60 * 1000;
export function installRuntimeFetchPatch(): void {
if (!isDesktopRuntime() || typeof window === 'undefined' || (window as unknown as Record<string, unknown>).__wmFetchPatched) {
return;
}
const nativeFetch = window.fetch.bind(window);
let localApiToken: string | null = null;
let tokenFetchedAt = 0;
let authRetryCooldownUntil = 0; // suppress 401 retries after consecutive failures
window.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
const target = getApiTargetFromRequestInput(input);
const debug = localStorage.getItem('wm-debug-log') === '1';
if (!target?.startsWith('/api/')) {
if (debug) {
const raw = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
console.log(`[fetch] passthrough → ${raw.slice(0, 120)}`);
}
return nativeFetch(input, init);
}
// Resolve dynamic sidecar port on first API call
if (_resolvedPort === null) {
try { await resolveLocalApiPort(); } catch { /* use default */ }
}
const tokenExpired = localApiToken && (Date.now() - tokenFetchedAt > TOKEN_TTL_MS);
if (!localApiToken || tokenExpired) {
try {
const { tryInvokeTauri } = await import('@/services/tauri-bridge');
localApiToken = await tryInvokeTauri<string>('get_local_api_token');
tokenFetchedAt = Date.now();
} catch {
localApiToken = null;
tokenFetchedAt = 0;
}
}
const headers = new Headers(init?.headers);
if (localApiToken) {
headers.set('Authorization', `Bearer ${localApiToken}`);
}
const localInit = { ...init, headers };
const localUrl = `${getApiBaseUrl()}${target}`;
if (debug) console.log(`[fetch] intercept → ${target}`);
let allowCloudFallback = !isLocalOnlyApiTarget(target);
if (allowCloudFallback && !isKeyFreeApiTarget(target)) {
try {
const { getSecretState, secretsReady } = await import('@/services/runtime-config');
await Promise.race([secretsReady, new Promise<void>(r => setTimeout(r, 2000))]);
const wmKeyState = getSecretState('WORLDMONITOR_API_KEY');
if (!wmKeyState.present || !wmKeyState.valid) {
allowCloudFallback = false;
}
} catch {
allowCloudFallback = false;
}
}
const cloudFallback = async () => {
if (!allowCloudFallback) {
throw new Error(`Cloud fallback blocked for ${target}`);
}
const cloudUrl = `${getRemoteApiBaseUrl()}${target}`;
if (debug) console.log(`[fetch] cloud fallback → ${cloudUrl}`);
const cloudHeaders = new Headers(init?.headers);
if (/^\/api\/[^/]+\/v1\//.test(target)) {
const { getRuntimeConfigSnapshot } = await import('@/services/runtime-config');
const wmKeyValue = getRuntimeConfigSnapshot().secrets['WORLDMONITOR_API_KEY']?.value;
if (wmKeyValue) {
cloudHeaders.set('X-WorldMonitor-Key', wmKeyValue);
}
}
return nativeFetch(cloudUrl, { ...init, headers: cloudHeaders });
};
try {
const t0 = performance.now();
let response = await fetchLocalWithStartupRetry(nativeFetch, localUrl, localInit);
if (debug) console.log(`[fetch] ${target}${response.status} (${Math.round(performance.now() - t0)}ms)`);
// Token may be stale after a sidecar restart — refresh and retry once.
// Skip retry if we recently failed (avoid doubling every request during auth outages).
if (response.status === 401 && localApiToken && Date.now() > authRetryCooldownUntil) {
if (debug) console.log(`[fetch] 401 from sidecar, refreshing token and retrying`);
try {
const { tryInvokeTauri } = await import('@/services/tauri-bridge');
localApiToken = await tryInvokeTauri<string>('get_local_api_token');
tokenFetchedAt = Date.now();
} catch {
localApiToken = null;
tokenFetchedAt = 0;
}
if (localApiToken) {
const retryHeaders = new Headers(init?.headers);
retryHeaders.set('Authorization', `Bearer ${localApiToken}`);
response = await fetchLocalWithStartupRetry(nativeFetch, localUrl, { ...init, headers: retryHeaders });
if (debug) console.log(`[fetch] retry ${target}${response.status}`);
if (response.status === 401) {
authRetryCooldownUntil = Date.now() + 60_000;
if (debug) console.log(`[fetch] auth retry failed, suppressing retries for 60s`);
} else {
authRetryCooldownUntil = 0;
}
}
}
if (!response.ok) {
if (!allowCloudFallback) {
if (debug) console.log(`[fetch] local-only endpoint ${target} returned ${response.status}; skipping cloud fallback`);
return response;
}
if (debug) console.log(`[fetch] local ${response.status}, falling back to cloud`);
return cloudFallback();
}
return response;
} catch (error) {
if (debug) console.warn(`[runtime] Local API unavailable for ${target}`, error);
if (!allowCloudFallback) {
throw error;
}
return cloudFallback();
}
};
(window as unknown as Record<string, unknown>).__wmFetchPatched = true;
}
const WEB_RPC_PATTERN = /^\/api\/[^/]+\/v1\//;
const ALLOWED_REDIRECT_HOSTS = /^https:\/\/([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)*worldmonitor\.app(:\d+)?$/;
function isAllowedRedirectTarget(url: string): boolean {
try {
const parsed = new URL(url);
return ALLOWED_REDIRECT_HOSTS.test(parsed.origin) || parsed.hostname === 'localhost';
} catch {
return false;
}
}
export function installWebApiRedirect(): void {
if (isDesktopRuntime() || typeof window === 'undefined') return;
if (!WS_API_URL) return;
if (!isAllowedRedirectTarget(WS_API_URL)) {
console.warn('[runtime] VITE_WS_API_URL blocked — not in hostname allowlist:', WS_API_URL);
return;
}
if ((window as unknown as Record<string, unknown>).__wmWebRedirectPatched) return;
const nativeFetch = window.fetch.bind(window);
const API_BASE = WS_API_URL;
window.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
if (typeof input === 'string' && WEB_RPC_PATTERN.test(input)) {
return nativeFetch(`${API_BASE}${input}`, init);
}
if (input instanceof URL && input.origin === window.location.origin && WEB_RPC_PATTERN.test(input.pathname)) {
return nativeFetch(new URL(`${API_BASE}${input.pathname}${input.search}`), init);
}
if (input instanceof Request) {
const u = new URL(input.url);
if (u.origin === window.location.origin && WEB_RPC_PATTERN.test(u.pathname)) {
return nativeFetch(new Request(`${API_BASE}${u.pathname}${u.search}`, input), init);
}
}
return nativeFetch(input, init);
};
(window as unknown as Record<string, unknown>).__wmWebRedirectPatched = true;
}