import { createHttpClient } from '@repo/core-api/http-client'; import { faroAdapter } from '@repo/core-api/observability'; import { ENV } from '../environment'; import { AppStorageKey, appStorage } from '../storage/local'; /** * Enterprise HTTP client for `apps/web`. * * - Full Faro observability via the shared `faroAdapter` * - Automatic Bearer token injection from localStorage * - 401 redirect to `/auth/login` * - Supports per-request `telemetryContext` for custom spans/tags * * All interceptors (auth, observability, error normalization) * are baked into this instance. Import this singleton throughout * the web application — never create raw axios instances. */ export const apiClient = createHttpClient( { baseURL: ENV.API_BASE_URL, timeout: 15000, observability: faroAdapter, }, { // ── Auth Interceptor ────────────────────────────────────────── onRequest: async (config) => { config.headers['ex-app-name'] = ENV.APP_NAME; config.headers['ex-app-version'] = ENV.APP_VERSION; config.headers['ex-timezone-offset-minutes'] = new Date().getTimezoneOffset(); config.headers['ex-timezone-offset-hours'] = Math.floor(new Date().getTimezoneOffset() / 60); const language = await appStorage.getItem(AppStorageKey.LANGUAGE); config.headers['ex-language'] = language ?? ENV.DEFAULT_LANGUAGE; const token = localStorage.getItem('access_token'); if (token) config.headers.Authorization = `Bearer ${token}`; return config; }, // ── Error Interceptor ───────────────────────────────────────── onResponseError: async (error) => { if (error.response?.status === 401) { // Clear stale token and redirect to login localStorage.removeItem('access_token'); window.location.href = '/auth/login'; } throw error; }, }, );