interface FlueFetchOptions { readonly baseUrl: URL; readonly fetchImpl?: typeof fetch; readonly generateRequestId?: () => string; } export const generateBrowserRequestId = (): string => { const bytes = new Uint8Array(16); if (globalThis.crypto?.getRandomValues) { globalThis.crypto.getRandomValues(bytes); } else { for (let index = 0; index < bytes.length; index += 1) { bytes[index] = Math.floor(Math.random() * 256); } } bytes[6] = ((bytes[6] ?? 0) % 16) + 64; bytes[8] = ((bytes[8] ?? 0) % 64) + 128; const hex = Array.from(bytes, (value) => value.toString(16).padStart(2, "0")); return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`; }; const addRequestContext = async ( response: Response, method: string, requestUrl: URL ): Promise => { if (response.ok) { return response; } const responseClone = response.clone(); const responseText = await responseClone.text(); const detail = responseText.trim() || "request failed"; return Response.json( { error: { message: `${method} ${requestUrl.pathname}${requestUrl.search}: ${detail}`, }, }, { headers: response.headers, status: response.status, statusText: response.statusText, } ); }; /** * Build the Flue transport used by the browser client. * * The installed Flue SDK does not expose per-call headers on `agents.send`. * Its custom `fetch` seam does receive the resolved URL and method, so request * IDs are attached here to each agent admission POST. History and Durable * Streams observation requests are GETs and intentionally remain untagged. */ export const createFlueFetch = ({ baseUrl, fetchImpl = fetch, generateRequestId = generateBrowserRequestId, }: FlueFetchOptions): typeof fetch => { const basePath = baseUrl.pathname.replace(/\/+$/u, ""); return async (input, init) => { const inputUrl = typeof input === "string" || input instanceof URL ? input : input.url; const requestUrl = new URL(inputUrl, baseUrl); const method = ( init?.method ?? (input instanceof Request ? input.method : "GET") ).toUpperCase(); const relativePath = requestUrl.pathname.startsWith(`${basePath}/`) ? requestUrl.pathname.slice(basePath.length) : requestUrl.pathname; const pathSegments = relativePath.split("/").filter(Boolean); const isAgentAdmission = method === "POST" && pathSegments.length === 3 && pathSegments[0] === "agents"; let requestInit = init; if (isAgentAdmission) { const headers = new Headers( input instanceof Request ? input.headers : undefined ); for (const [key, value] of new Headers(init?.headers).entries()) { headers.set(key, value); } headers.set("x-zopu-request-id", generateRequestId()); requestInit = { ...init, headers }; } const rawResponse = await fetchImpl.call(globalThis, input, requestInit); const response = await addRequestContext(rawResponse, method, requestUrl); if (!response.headers.get("content-type")?.includes("application/json")) { return response; } const body = (await response.clone().json()) as unknown; if ( typeof body !== "object" || body === null || !("streamUrl" in body) || typeof body.streamUrl !== "string" ) { return response; } const streamUrl = new URL(body.streamUrl); streamUrl.protocol = baseUrl.protocol; streamUrl.host = baseUrl.host; const headers = new Headers(response.headers); headers.set("location", streamUrl.toString()); return Response.json( { ...body, streamUrl: streamUrl.toString() }, { headers, status: response.status, statusText: response.statusText, } ); }; };