import { IPC_EVENTS } from './constants'; import type { HostApiResult } from '../types/runtime'; type RequestInitLike = Pick; type LooseIpcBridge = { invoke(channel: string, ...args: any[]): Promise; on?(channel: string, callback: (...args: any[]) => void): () => void; }; function normalizeHeaders(headers?: HeadersInit): Headers { return new Headers(headers ?? {}); } function readCookie(name: string): string | null { if (typeof document === 'undefined' || !document.cookie) return null; const match = document.cookie.match(new RegExp(`(?:^|; )${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}=([^;]*)`)); return match ? decodeURIComponent(match[1]) : null; } function readToken(): string | null { if (typeof window === 'undefined') return null; const storageCandidates = [window.sessionStorage, window.localStorage]; for (const storage of storageCandidates) { for (const key of ['token', 'access_token', 'refresh_token']) { const value = storage.getItem(key); if (value) return value; } } return readCookie('token') ?? readCookie('access_token') ?? readCookie('refresh_token'); } function normalizeBody(body: BodyInit | null | undefined): BodyInit | null | undefined { if (body == null) return body; if ( typeof body === 'string' || body instanceof Blob || body instanceof FormData || body instanceof URLSearchParams || body instanceof ArrayBuffer ) { return body; } if (ArrayBuffer.isView(body)) { return body; } return JSON.stringify(body); } function extractResult(response: unknown): T { if (response && typeof response === 'object') { const result = response as HostApiResult; if (result.success === false || result.ok === false) { throw new Error(result.error || result.text || 'Request failed'); } if (typeof result.json !== 'undefined') return result.json; if (typeof result.data !== 'undefined') { const data = result.data as { json?: T } | T; return (data && typeof data === 'object' && 'json' in data ? data.json : data) as T; } } return response as T; } export function hasHostApiBridge(): boolean { return typeof window !== 'undefined' && Boolean(window.api?.invoke); } export async function invokeIpc(channel: string, ...args: any[]): Promise { if (!hasHostApiBridge()) { throw new Error(`IPC bridge is unavailable for ${channel}`); } const bridge = window.api as unknown as LooseIpcBridge; return bridge.invoke(channel, ...args); } export function onIpc(channel: string, callback: (...args: any[]) => void): () => void { if (!hasHostApiBridge() || !window.api?.on) { return () => {}; } const bridge = window.api as unknown as LooseIpcBridge; return bridge.on ? bridge.on(channel, callback) : () => {}; } export async function hostApiFetch(path: string, init?: RequestInitLike): Promise { const method = init?.method ?? 'GET'; const headers = normalizeHeaders(init?.headers); const token = readToken(); if (token && !headers.has('Authorization')) { headers.set('Authorization', `Bearer ${token}`); } const request = { path, method, headers: (() => { const headerObject: Record = {}; headers.forEach((value, key) => { headerObject[key] = value; }); return headerObject; })(), body: init?.body ?? null, }; if (hasHostApiBridge()) { const response = await invokeIpc(IPC_EVENTS.HOST_API_FETCH, request); return extractResult(response); } if (typeof fetch === 'function') { const response = await fetch(path, { method, headers, body: normalizeBody(init?.body), }); if (!response.ok) { const text = await response.text(); throw new Error(text || response.statusText || `Request failed with ${response.status}`); } const contentType = response.headers.get('content-type') ?? ''; if (contentType.includes('application/json')) { return (await response.json()) as T; } return (await response.text()) as unknown as T; } throw new Error(`No HTTP bridge available for ${path}`); }