// Modified in the independent hardening fork; see docs/hardening/README.md. // Modified for AgentDeskFixes: scope authentication expiry to the session that issued each request. import { expireSession, readSession, type AuthSession } from "@/lib/auth" import { isSameSession } from "@/lib/session-request-guard" import { translateCurrentMessage } from "@/i18n/messages" const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL?.trim() || "" type JsonResult = { errorCode: number message: string data: T success: boolean } type RequestOptions = RequestInit & { skipAuth?: boolean baseUrl?: string onResponse?: (response: Response) => void } export type BlobResponse = { blob: Blob filename: string } export class ApiRequestError extends Error { errorCode?: number data?: unknown constructor(message: string, errorCode?: number, data?: unknown, options?: ErrorOptions) { super(message, options) this.name = "ApiRequestError" this.errorCode = errorCode this.data = data } } export function isApiRequestError(error: unknown): error is ApiRequestError { return error instanceof ApiRequestError } function normalizeApiRequestError(error: unknown) { if (isApiRequestError(error)) { return error } return new ApiRequestError( translateCurrentMessage("api.requestFailed"), undefined, undefined, { cause: error } ) } async function parseResult(response: Response, requestSession: AuthSession | null) { const payload = (await response.json()) as JsonResult if (!response.ok || !payload.success) { if ((payload.errorCode === 3000 || payload.errorCode === 3002) && isSameSession(requestSession, readSession())) { expireSession() } throw new ApiRequestError( payload.message || translateCurrentMessage("api.requestFailed"), payload.errorCode, payload.data ) } return payload.data } function buildRequestHeaders(headers: HeadersInit | undefined, session: AuthSession | null, body?: BodyInit | null) { const authHeaders = new Headers(headers) if (session?.accessToken) { authHeaders.set("Authorization", `Bearer ${session.accessToken}`) } if ( !authHeaders.has("Content-Type") && body && !(typeof FormData !== "undefined" && body instanceof FormData) ) { authHeaders.set("Content-Type", "application/json") } return authHeaders } function parseFilename(contentDisposition: string | null) { if (!contentDisposition) { return "" } const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i) if (utf8Match?.[1]) { return decodeURIComponent(utf8Match[1]) } const match = contentDisposition.match(/filename="?([^";]+)"?/i) return match?.[1] ? decodeURIComponent(match[1]) : "" } export async function request( path: string, options: RequestOptions = {} ): Promise { const { headers, skipAuth, baseUrl, onResponse, ...rest } = options delete (rest as RequestOptions).baseUrl delete (rest as RequestOptions).onResponse const requestSession = skipAuth ? null : readSession() const authHeaders = buildRequestHeaders(headers, requestSession, rest.body) const requestBaseUrl = baseUrl !== undefined ? baseUrl : API_BASE_URL try { const response = await fetch(`${requestBaseUrl}${path}`, { ...rest, headers: authHeaders, cache: "no-store", }) onResponse?.(response) return await parseResult(response, requestSession) } catch (error) { throw normalizeApiRequestError(error) } } export async function requestBlob( path: string, options: RequestOptions = {} ): Promise { const { headers, skipAuth, baseUrl, onResponse, ...rest } = options delete (rest as RequestOptions).baseUrl delete (rest as RequestOptions).onResponse const requestSession = skipAuth ? null : readSession() const authHeaders = buildRequestHeaders(headers, requestSession, rest.body) const requestBaseUrl = baseUrl !== undefined ? baseUrl : API_BASE_URL const response = await fetch(`${requestBaseUrl}${path}`, { ...rest, headers: authHeaders, cache: "no-store", }) onResponse?.(response) const contentType = response.headers.get("Content-Type") ?? "" if (contentType.includes("application/json")) { try { await parseResult(response, requestSession) } catch (error) { throw error } throw new ApiRequestError(translateCurrentMessage("api.requestFailed")) } if (!response.ok) { throw new ApiRequestError(response.statusText || translateCurrentMessage("api.requestFailed")) } return { blob: await response.blob(), filename: parseFilename(response.headers.get("Content-Disposition")), } }