import { HttpResponseError } from "./backend-availability"; import { withClientRequestDeadline } from "./client-request-deadline"; import { confirmSessionAfterUnauthorized, isSessionInvalidResponse } from "./session-revocation"; import { getSupabase } from "./supabase"; import { takeServerSeed } from "./server-seed"; export const CLIENT_READ_TIMEOUT_MS = 30_000; /** * Renew browser cookies before protected reads. Foreground requests can race * the auth refresh ticker; preparing here lets concurrent reads share renewal. * The SDK shares concurrent renewals; getSession is not an authorization check. */ export async function prepareClientSession(): Promise { const { data, error } = await getSupabase().auth.getSession(); if (error) throw error; if (!data.session) throw new HttpResponseError("Unauthorized", 401); return data.session.user.id; } /** Protected reads only. Archive transfers may opt out of the read deadline. */ export function fetchClientRead( path: string, init?: RequestInit, { timeoutMs = CLIENT_READ_TIMEOUT_MS }: { timeoutMs?: number | null } = {}, ): Promise { if (!path.startsWith("/api/") || (init?.method ?? "GET").toUpperCase() !== "GET") { throw new Error("Client reads require a relative API path and GET method"); } return withClientRequestDeadline(async (signal) => { // Unbounded archive transfers still bound both session preparations. const prepare = () => timeoutMs === null ? withClientRequestDeadline(prepareClientSession, CLIENT_READ_TIMEOUT_MS, signal) : prepareClientSession(); const owner = await prepare(); signal.throwIfAborted(); // The document may already carry this read (MIN-880, lib/server-seed.ts). const seeded = takeServerSeed(path, owner); const response = (seeded && await seeded) || await fetch(path, { ...init, signal }); // Include body consumption in the deadline: headers alone do not finish a read. const body = await response.arrayBuffer(); signal.throwIfAborted(); if (await prepare() !== owner) throw new Error("Local account changed"); signal.throwIfAborted(); if (isSessionInvalidResponse(response.status, body)) void confirmSessionAfterUnauthorized(path); return new Response(response.status === 204 || response.status === 304 ? null : body, { status: response.status, statusText: response.statusText, headers: response.headers, }); }, timeoutMs, init?.signal); }