const API_BASE_URL = process.env.NEXT_PUBLIC_MESHKEE_API_URL?.replace(/\/$/, "") || "http://localhost:3010/api/v1"; const ACCESS_TOKEN_KEY = "marketplace_access_token"; const REFRESH_TOKEN_KEY = "marketplace_refresh_token"; const TOKEN_MAX_AGE_SEC = 60 * 60 * 24 * 14; // 14 days export class ApiError extends Error { status: number; constructor(message: string, status: number) { super(message); this.name = "ApiError"; this.status = status; } } export function isAbortError(err: unknown): boolean { return err instanceof DOMException && err.name === "AbortError"; } /** Share cookies across admin / customer / business hosts on the same root. */ function cookieDomain(): string | undefined { const root = (process.env.NEXT_PUBLIC_ROOT_DOMAIN || "localhost").trim(); if (!root || root === "localhost" || root.startsWith("127.")) { return "localhost"; } return root.startsWith(".") ? root : `.${root}`; } function readCookie(name: string): string | null { if (typeof document === "undefined") return null; const prefix = `${name}=`; for (const part of document.cookie.split(";")) { const trimmed = part.trim(); if (trimmed.startsWith(prefix)) { return decodeURIComponent(trimmed.slice(prefix.length)); } } return null; } function writeCookie(name: string, value: string) { if (typeof document === "undefined") return; const domain = cookieDomain(); const secure = typeof window !== "undefined" && window.location.protocol === "https:" ? "; Secure" : ""; document.cookie = `${name}=${encodeURIComponent(value)}; Path=/; Max-Age=${TOKEN_MAX_AGE_SEC}; SameSite=Lax; Domain=${domain}${secure}`; } function deleteCookie(name: string) { if (typeof document === "undefined") return; const domain = cookieDomain(); document.cookie = `${name}=; Path=/; Max-Age=0; SameSite=Lax; Domain=${domain}`; } export function getAccessToken() { if (typeof window === "undefined") return null; return ( readCookie(ACCESS_TOKEN_KEY) || localStorage.getItem(ACCESS_TOKEN_KEY) ); } export function getRefreshToken() { if (typeof window === "undefined") return null; return ( readCookie(REFRESH_TOKEN_KEY) || localStorage.getItem(REFRESH_TOKEN_KEY) ); } export function setTokens(accessToken: string, refreshToken: string) { localStorage.setItem(ACCESS_TOKEN_KEY, accessToken); localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken); writeCookie(ACCESS_TOKEN_KEY, accessToken); writeCookie(REFRESH_TOKEN_KEY, refreshToken); } export function clearTokens() { localStorage.removeItem(ACCESS_TOKEN_KEY); localStorage.removeItem(REFRESH_TOKEN_KEY); deleteCookie(ACCESS_TOKEN_KEY); deleteCookie(REFRESH_TOKEN_KEY); } /** Promote existing localStorage sessions into shared cookies (once per load). */ export function migrateTokensToSharedCookies() { if (typeof window === "undefined") return; const access = localStorage.getItem(ACCESS_TOKEN_KEY); const refresh = localStorage.getItem(REFRESH_TOKEN_KEY); if (access && refresh && !readCookie(ACCESS_TOKEN_KEY)) { writeCookie(ACCESS_TOKEN_KEY, access); writeCookie(REFRESH_TOKEN_KEY, refresh); } } function parseErrorMessage(payload: unknown, fallback: string) { if (!payload || typeof payload !== "object") { return fallback; } const message = (payload as { message?: string | string[] }).message; if (Array.isArray(message)) { return message.join(", "); } if (typeof message === "string") { return message; } return fallback; } async function refreshAccessToken() { const refreshToken = getRefreshToken(); if (!refreshToken) { return false; } const response = await fetch(`${API_BASE_URL}/auth/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken }), }); if (!response.ok) { clearTokens(); return false; } const data = await response.json(); setTokens(data.accessToken, data.refreshToken); return true; } interface RequestOptions extends Omit { body?: unknown; auth?: boolean; } export async function apiRequest( path: string, options: RequestOptions = {}, ): Promise { const { body, auth = false, headers, ...rest } = options; const requestHeaders = new Headers(headers); if (body !== undefined) { requestHeaders.set("Content-Type", "application/json"); } if (auth) { const accessToken = getAccessToken(); if (accessToken) { requestHeaders.set("Authorization", `Bearer ${accessToken}`); } } const response = await fetch(`${API_BASE_URL}${path}`, { ...rest, headers: requestHeaders, body: body !== undefined ? JSON.stringify(body) : undefined, }); if (response.status === 401 && auth) { const refreshed = await refreshAccessToken(); if (refreshed) { return apiRequest(path, options); } } const payload = await response.json().catch(() => null); if (!response.ok) { throw new ApiError( parseErrorMessage(payload, `Request failed with status ${response.status}`), response.status, ); } return payload as T; }