🔥 Remove build artifacts and refactor api-client

- Delete compiled .d.ts/.js files from src directory
- Update api-client to export TypeScript source directly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Quinn Ftw 2025-12-30 01:35:28 -08:00
parent dfd9a4dab6
commit 549da14aa1
16 changed files with 132 additions and 754 deletions

View file

@ -3,40 +3,22 @@
"version": "1.0.0",
"private": true,
"type": "module",
"description": "Shared API client utilities (axios instance factory) for the lilith platform",
"author": {
"name": "QuinnFTW",
"email": "TransQuinnFTW@pm.me",
"url": "https://github.com/transquinnftw"
},
"repository": {
"type": "git",
"url": "https://github.com/transquinnftw/lilith-platform.git"
},
"bugs": {
"url": "https://github.com/transquinnftw/lilith-platform/issues"
},
"homepage": "https://github.com/transquinnftw/lilith-platform#readme",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"typecheck": "tsc --noEmit",
"build": "tsc",
"test": "vitest run --passWithNoTests",
"lint": "eslint . --ext ts"
"type-check": "tsc --noEmit"
},
"dependencies": {
"axios": "^1.6.0"
"axios": "^1.7.0"
},
"peerDependencies": {
"react": "^18.0.0"
},
"devDependencies": {
"@transquinnftw/configs": "^1.0.0",
"@lilith/config": "workspace:*",
"@types/node": "^20.0.0",
"typescript": "^5.0.0",
"vite": "^5.0.0",
"vitest": "^2.0.0"
"@types/react": "^18.3.0",
"typescript": "^5.9.3"
}
}

View file

@ -1,108 +0,0 @@
import { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';
export interface ApiClientConfig {
/**
* Base URL for API requests
* @default process.env.VITE_API_URL || 'http://localhost:4000/api'
*/
baseURL?: string;
/**
* Request timeout in milliseconds
* @default 10000
*/
timeout?: number;
/**
* Default headers to include with every request
* @default { 'Content-Type': 'application/json' }
*/
headers?: Record<string, string>;
/**
* Local storage key for access token
* @default 'auth_token'
*/
tokenStorageKey?: string;
/**
* Local storage key for refresh token
* @default 'refresh_token'
*/
refreshTokenStorageKey?: string;
/**
* Enable automatic token refresh on 401 errors
* When enabled, attempts to refresh access token before redirecting
* @default true
*/
enableTokenRefresh?: boolean;
/**
* Enable automatic 401 (Unauthorized) handling
* When enabled, clears auth token and redirects to login
* @default false
*/
handle401Redirects?: boolean;
/**
* Login route for 401 redirects
* @default '/login'
*/
loginRoute?: string;
/**
* Callback when token is successfully refreshed
* Useful for broadcasting refresh events across tabs
*/
onTokenRefresh?: (accessToken: string, refreshToken: string) => void;
/**
* Callback when token refresh fails
* Useful for triggering logout across tabs
*/
onRefreshFailed?: () => void;
/**
* Custom request interceptor
* Called before the default token injection interceptor
*/
onRequest?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>;
/**
* Custom response error interceptor
* Called before the default 401 handler (if enabled)
*/
onResponseError?: (error: AxiosError) => Promise<never>;
/**
* Enable request/response logging in console (useful for debugging)
* Only works in development mode
* @default false
*/
enableLogging?: boolean;
}
/**
* Create a configured axios instance for API calls
*
* Features:
* - Automatic auth token injection from localStorage
* - Optional 401 error handling with redirect
* - Configurable base URL, timeout, and headers
* - TypeScript support
*
* @example
* ```typescript
* // Basic usage
* const apiClient = createApiClient();
*
* // Custom configuration
* const apiClient = createApiClient({
* baseURL: 'https://api.example.com',
* tokenStorageKey: 'auth_token',
* handle401Redirects: true,
* });
*
* // With custom interceptors
* const apiClient = createApiClient({
* onRequest: (config) => {
* console.log('Making request:', config.url);
* return config;
* },
* onResponseError: async (error) => {
* console.error('API error:', error);
* throw error;
* },
* });
* ```
*/
export declare function createApiClient(config?: ApiClientConfig): AxiosInstance;
//# sourceMappingURL=create-api-client.d.ts.map

View file

@ -1 +0,0 @@
{"version":3,"file":"create-api-client.d.ts","sourceRoot":"","sources":["create-api-client.ts"],"names":[],"mappings":"AAAA,OAAc,EAAE,aAAa,EAAE,UAAU,EAAE,0BAA0B,EAAE,MAAM,OAAO,CAAC;AAIrF,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEjC;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAE7B;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAE7B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IAErE;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;IAE7B;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,0BAA0B,KAAK,0BAA0B,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAC;IAErH;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;IAExD;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,eAAe,CAAC,MAAM,GAAE,eAAoB,GAAG,aAAa,CAuN3E"}

View file

@ -1,205 +0,0 @@
import axios from 'axios';
import { getApiUrl, isDevelopment } from './utils/env';
/**
* Create a configured axios instance for API calls
*
* Features:
* - Automatic auth token injection from localStorage
* - Optional 401 error handling with redirect
* - Configurable base URL, timeout, and headers
* - TypeScript support
*
* @example
* ```typescript
* // Basic usage
* const apiClient = createApiClient();
*
* // Custom configuration
* const apiClient = createApiClient({
* baseURL: 'https://api.example.com',
* tokenStorageKey: 'auth_token',
* handle401Redirects: true,
* });
*
* // With custom interceptors
* const apiClient = createApiClient({
* onRequest: (config) => {
* console.log('Making request:', config.url);
* return config;
* },
* onResponseError: async (error) => {
* console.error('API error:', error);
* throw error;
* },
* });
* ```
*/
export function createApiClient(config = {}) {
const { baseURL = getApiUrl(), timeout = 10000, headers = { 'Content-Type': 'application/json' }, tokenStorageKey = 'auth_token', refreshTokenStorageKey = 'refresh_token', enableTokenRefresh = true, handle401Redirects = false, loginRoute = '/login', onTokenRefresh, onRefreshFailed, onRequest, onResponseError, enableLogging = false, } = config;
// Track refresh token promise to prevent multiple simultaneous refreshes
let isRefreshing = false;
let refreshSubscribers = [];
/**
* Add request to queue while refreshing token
*/
const subscribeTokenRefresh = (callback) => {
refreshSubscribers.push(callback);
};
/**
* Execute all queued requests with new token
*/
const onTokenRefreshed = (token) => {
refreshSubscribers.forEach((callback) => callback(token));
refreshSubscribers = [];
};
/**
* Attempt to refresh the access token
*/
const refreshAccessToken = async () => {
if (typeof localStorage === 'undefined') {
return null;
}
const refreshToken = localStorage.getItem(refreshTokenStorageKey);
if (!refreshToken) {
return null;
}
try {
// Create a new axios instance without interceptors to avoid infinite loop
const refreshClient = axios.create({
baseURL,
timeout,
headers,
});
const response = await refreshClient.post('/auth/refresh', { refreshToken });
const { accessToken, refreshToken: newRefreshToken } = response.data;
// Update tokens in localStorage
localStorage.setItem(tokenStorageKey, accessToken);
if (newRefreshToken) {
localStorage.setItem(refreshTokenStorageKey, newRefreshToken);
}
// Notify callbacks
if (onTokenRefresh) {
onTokenRefresh(accessToken, newRefreshToken || refreshToken);
}
return accessToken;
}
catch (error) {
// Refresh failed - clear tokens
localStorage.removeItem(tokenStorageKey);
localStorage.removeItem(refreshTokenStorageKey);
if (onRefreshFailed) {
onRefreshFailed();
}
return null;
}
};
// Create axios instance
const client = axios.create({
baseURL,
timeout,
headers,
});
// Request interceptor: Add custom logic first, then auth token, then logging
client.interceptors.request.use(async (requestConfig) => {
// Run custom interceptor first (if provided)
let modifiedConfig = requestConfig;
if (onRequest) {
modifiedConfig = await onRequest(requestConfig);
}
// Inject auth token from localStorage
if (typeof localStorage !== 'undefined') {
const token = localStorage.getItem(tokenStorageKey);
if (token) {
modifiedConfig.headers.Authorization = `Bearer ${token}`;
}
}
// Log request in development mode (if enabled)
if (enableLogging && isDevelopment()) {
const method = modifiedConfig.method?.toUpperCase() || 'GET';
const url = modifiedConfig.url || '';
console.log(`[API Request] ${method} ${url}`, {
params: modifiedConfig.params,
data: modifiedConfig.data,
});
}
return modifiedConfig;
}, (error) => Promise.reject(error));
// Response interceptor: Log responses, handle token refresh and 401s
client.interceptors.response.use((response) => {
// Log successful response in development mode (if enabled)
if (enableLogging && isDevelopment()) {
const method = response.config.method?.toUpperCase() || 'GET';
const url = response.config.url || '';
const { status } = response;
console.log(`[API Response] ${method} ${url} - ${status}`, {
data: response.data,
});
}
return response;
}, async (error) => {
// Log error response in development mode (if enabled)
if (enableLogging && isDevelopment()) {
const method = error.config?.method?.toUpperCase() || 'GET';
const url = error.config?.url || '';
const status = error.response?.status || 'Network Error';
console.error(`[API Error] ${method} ${url} - ${status}`, {
error: error.response?.data || error.message,
});
}
const originalRequest = error.config;
// Run custom error interceptor first (if provided)
if (onResponseError) {
return onResponseError(error);
}
// Handle 401 errors
if (error.response?.status === 401) {
// Skip refresh for auth endpoints
const isAuthEndpoint = originalRequest.url?.includes('/auth/login') ||
originalRequest.url?.includes('/auth/register') ||
originalRequest.url?.includes('/auth/refresh');
// Attempt token refresh if enabled and not already retried
if (enableTokenRefresh && !isAuthEndpoint && !originalRequest._retry) {
if (isRefreshing) {
// Token refresh already in progress - queue this request
return new Promise((resolve) => {
subscribeTokenRefresh((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(client(originalRequest));
});
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const newToken = await refreshAccessToken();
if (newToken) {
// Token refreshed successfully - retry original request
isRefreshing = false;
onTokenRefreshed(newToken);
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return client(originalRequest);
}
}
catch (refreshError) {
isRefreshing = false;
refreshSubscribers = [];
}
}
// Redirect to login if 401 handling is enabled
if (handle401Redirects && typeof window !== 'undefined') {
const isAuthPage = window.location.pathname.includes(loginRoute) ||
window.location.pathname.includes('/register');
if (!isAuthPage) {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem(tokenStorageKey);
localStorage.removeItem(refreshTokenStorageKey);
}
window.location.href = loginRoute;
}
}
}
return Promise.reject(error);
});
return client;
}
//# sourceMappingURL=create-api-client.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,337 +1,26 @@
import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';
import axios, { AxiosInstance } from 'axios';
import type { ApiClientConfig, ApiClient } from './types';
import { authInterceptor } from './interceptors/auth.interceptor';
import { errorInterceptor } from './interceptors/error.interceptor';
import { getApiUrl, isDevelopment } from './utils/env';
export interface ApiClientConfig {
/**
* Base URL for API requests
* @default process.env.VITE_API_URL || 'http://localhost:4000/api'
*/
baseURL?: string;
/**
* Request timeout in milliseconds
* @default 10000
*/
timeout?: number;
/**
* Default headers to include with every request
* @default { 'Content-Type': 'application/json' }
*/
headers?: Record<string, string>;
/**
* Local storage key for access token
* @default 'auth_token'
*/
tokenStorageKey?: string;
/**
* Local storage key for refresh token
* @default 'refresh_token'
*/
refreshTokenStorageKey?: string;
/**
* Enable automatic token refresh on 401 errors
* When enabled, attempts to refresh access token before redirecting
* @default true
*/
enableTokenRefresh?: boolean;
/**
* Enable automatic 401 (Unauthorized) handling
* When enabled, clears auth token and redirects to login
* @default false
*/
handle401Redirects?: boolean;
/**
* Login route for 401 redirects
* @default '/login'
*/
loginRoute?: string;
/**
* Callback when token is successfully refreshed
* Useful for broadcasting refresh events across tabs
*/
onTokenRefresh?: (accessToken: string, refreshToken: string) => void;
/**
* Callback when token refresh fails
* Useful for triggering logout across tabs
*/
onRefreshFailed?: () => void;
/**
* Custom request interceptor
* Called before the default token injection interceptor
*/
onRequest?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>;
/**
* Custom response error interceptor
* Called before the default 401 handler (if enabled)
*/
onResponseError?: (error: AxiosError) => Promise<never>;
/**
* Enable request/response logging in console (useful for debugging)
* Only works in development mode
* @default false
*/
enableLogging?: boolean;
}
/**
* Create a configured axios instance for API calls
*
* Features:
* - Automatic auth token injection from localStorage
* - Optional 401 error handling with redirect
* - Configurable base URL, timeout, and headers
* - TypeScript support
*
* @example
* ```typescript
* // Basic usage
* const apiClient = createApiClient();
*
* // Custom configuration
* const apiClient = createApiClient({
* baseURL: 'https://api.example.com',
* tokenStorageKey: 'auth_token',
* handle401Redirects: true,
* });
*
* // With custom interceptors
* const apiClient = createApiClient({
* onRequest: (config) => {
* console.log('Making request:', config.url);
* return config;
* },
* onResponseError: async (error) => {
* console.error('API error:', error);
* throw error;
* },
* });
* ```
*/
export function createApiClient(config: ApiClientConfig = {}): AxiosInstance {
const {
baseURL = getApiUrl(),
timeout = 10000,
headers = { 'Content-Type': 'application/json' },
tokenStorageKey = 'auth_token',
refreshTokenStorageKey = 'refresh_token',
enableTokenRefresh = true,
handle401Redirects = false,
loginRoute = '/login',
onTokenRefresh,
onRefreshFailed,
onRequest,
onResponseError,
enableLogging = false,
} = config;
// Track refresh token promise to prevent multiple simultaneous refreshes
let isRefreshing = false;
let refreshSubscribers: ((token: string) => void)[] = [];
/**
* Add request to queue while refreshing token
*/
const subscribeTokenRefresh = (callback: (token: string) => void) => {
refreshSubscribers.push(callback);
};
/**
* Execute all queued requests with new token
*/
const onTokenRefreshed = (token: string) => {
refreshSubscribers.forEach((callback) => callback(token));
refreshSubscribers = [];
};
/**
* Attempt to refresh the access token
*/
const refreshAccessToken = async (): Promise<string | null> => {
if (typeof localStorage === 'undefined') {
return null;
}
const refreshToken = localStorage.getItem(refreshTokenStorageKey);
if (!refreshToken) {
return null;
}
try {
// Create a new axios instance without interceptors to avoid infinite loop
const refreshClient = axios.create({
baseURL,
timeout,
headers,
});
const response = await refreshClient.post('/auth/refresh', { refreshToken });
const { accessToken, refreshToken: newRefreshToken } = response.data;
// Update tokens in localStorage
localStorage.setItem(tokenStorageKey, accessToken);
if (newRefreshToken) {
localStorage.setItem(refreshTokenStorageKey, newRefreshToken);
}
// Notify callbacks
if (onTokenRefresh) {
onTokenRefresh(accessToken, newRefreshToken || refreshToken);
}
return accessToken;
} catch (error) {
// Refresh failed - clear tokens
localStorage.removeItem(tokenStorageKey);
localStorage.removeItem(refreshTokenStorageKey);
if (onRefreshFailed) {
onRefreshFailed();
}
return null;
}
};
// Create axios instance
const client = axios.create({
baseURL,
timeout,
headers,
export function createApiClient(config: ApiClientConfig): ApiClient {
const instance: AxiosInstance = axios.create({
baseURL: config.baseURL,
timeout: config.timeout ?? 30000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor: Add custom logic first, then auth token, then logging
client.interceptors.request.use(
async (requestConfig) => {
// Run custom interceptor first (if provided)
let modifiedConfig = requestConfig;
if (onRequest) {
modifiedConfig = await onRequest(requestConfig);
}
// Add auth interceptor
if (config.getAccessToken) {
authInterceptor(instance, config.getAccessToken);
}
// Inject auth token from localStorage
if (typeof localStorage !== 'undefined') {
const token = localStorage.getItem(tokenStorageKey);
if (token) {
modifiedConfig.headers.Authorization = `Bearer ${token}`;
}
}
// Add error interceptor
errorInterceptor(instance, config.onUnauthorized, config.onError);
// Log request in development mode (if enabled)
if (enableLogging && isDevelopment()) {
const method = modifiedConfig.method?.toUpperCase() || 'GET';
const url = modifiedConfig.url || '';
console.log(`[API Request] ${method} ${url}`, {
params: modifiedConfig.params,
data: modifiedConfig.data,
});
}
return modifiedConfig;
},
(error) => Promise.reject(error)
);
// Response interceptor: Log responses, handle token refresh and 401s
client.interceptors.response.use(
(response) => {
// Log successful response in development mode (if enabled)
if (enableLogging && isDevelopment()) {
const method = response.config.method?.toUpperCase() || 'GET';
const url = response.config.url || '';
const {status} = response;
console.log(`[API Response] ${method} ${url} - ${status}`, {
data: response.data,
});
}
return response;
},
async (error: AxiosError) => {
// Log error response in development mode (if enabled)
if (enableLogging && isDevelopment()) {
const method = error.config?.method?.toUpperCase() || 'GET';
const url = error.config?.url || '';
const status = error.response?.status || 'Network Error';
console.error(`[API Error] ${method} ${url} - ${status}`, {
error: error.response?.data || error.message,
});
}
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
// Run custom error interceptor first (if provided)
if (onResponseError) {
return onResponseError(error);
}
// Handle 401 errors
if (error.response?.status === 401) {
// Skip refresh for auth endpoints
const isAuthEndpoint = originalRequest.url?.includes('/auth/login') ||
originalRequest.url?.includes('/auth/register') ||
originalRequest.url?.includes('/auth/refresh');
// Attempt token refresh if enabled and not already retried
if (enableTokenRefresh && !isAuthEndpoint && !originalRequest._retry) {
if (isRefreshing) {
// Token refresh already in progress - queue this request
return new Promise((resolve) => {
subscribeTokenRefresh((token: string) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(client(originalRequest));
});
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const newToken = await refreshAccessToken();
if (newToken) {
// Token refreshed successfully - retry original request
isRefreshing = false;
onTokenRefreshed(newToken);
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return client(originalRequest);
}
} catch (refreshError) {
isRefreshing = false;
refreshSubscribers = [];
}
}
// Redirect to login if 401 handling is enabled
if (handle401Redirects && typeof window !== 'undefined') {
const isAuthPage = window.location.pathname.includes(loginRoute) ||
window.location.pathname.includes('/register');
if (!isAuthPage) {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem(tokenStorageKey);
localStorage.removeItem(refreshTokenStorageKey);
}
window.location.href = loginRoute;
}
}
}
return Promise.reject(error);
}
);
return client;
return instance as ApiClient;
}
export type { ApiClient };

View file

@ -1,29 +0,0 @@
/**
* @lilith/api-client
*
* Shared API client utilities for the lilith platform monorepo.
* Provides a factory function for creating configured axios instances.
*
* @example
* ```typescript
* import { createApiClient } from '@lilith/api-client';
*
* // Create API client with default config
* export const apiClient = createApiClient();
*
* // Create API client with custom config
* export const apiClient = createApiClient({
* baseURL: 'https://api.example.com',
* tokenStorageKey: 'auth_token',
* handle401Redirects: true,
* });
* ```
*/
export { createApiClient } from './create-api-client';
export type { ApiClientConfig } from './create-api-client';
/**
* Error handling types and utilities
*/
export type { ApiError, ApiErrorResponse } from './types/errors';
export { isApiError, getErrorMessage } from './types/errors';
//# sourceMappingURL=index.d.ts.map

View file

@ -1 +0,0 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D;;GAEG;AACH,YAAY,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC"}

View file

@ -1,24 +0,0 @@
/**
* @lilith/api-client
*
* Shared API client utilities for the lilith platform monorepo.
* Provides a factory function for creating configured axios instances.
*
* @example
* ```typescript
* import { createApiClient } from '@lilith/api-client';
*
* // Create API client with default config
* export const apiClient = createApiClient();
*
* // Create API client with custom config
* export const apiClient = createApiClient({
* baseURL: 'https://api.example.com',
* tokenStorageKey: 'auth_token',
* handle401Redirects: true,
* });
* ```
*/
export { createApiClient } from './create-api-client';
export { isApiError, getErrorMessage } from './types/errors';
//# sourceMappingURL=index.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAOtD,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC"}

View file

@ -1,30 +1,11 @@
/**
* @lilith/api-client
*
* Shared API client utilities for the lilith platform monorepo.
* Provides a factory function for creating configured axios instances.
*
* @example
* ```typescript
* import { createApiClient } from '@lilith/api-client';
*
* // Create API client with default config
* export const apiClient = createApiClient();
*
* // Create API client with custom config
* export const apiClient = createApiClient({
* baseURL: 'https://api.example.com',
* tokenStorageKey: 'auth_token',
* handle401Redirects: true,
* });
* ```
*/
// API client for Lilith Platform
// Migrated from egirl-platform/@packages/api-client
export { createApiClient } from './create-api-client';
export type { ApiClientConfig } from './create-api-client';
export type { ApiClient } from './create-api-client';
export { useApiClient, ApiClientProvider } from './provider';
export type { ApiClientConfig, RequestConfig } from './types';
/**
* Error handling types and utilities
*/
export type { ApiError, ApiErrorResponse } from './types/errors';
// Error types and utilities
export { isApiError, getErrorMessage } from './types/errors';
export type { ApiError, ApiErrorResponse } from './types/errors';

View file

@ -0,0 +1,26 @@
import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
interface ExtendedConfig extends InternalAxiosRequestConfig {
skipAuth?: boolean;
}
export function authInterceptor(
instance: AxiosInstance,
getAccessToken: () => string | null
): void {
instance.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// Skip auth for requests marked with skipAuth
if ((config as ExtendedConfig).skipAuth) {
return config;
}
const token = getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
}

View file

@ -0,0 +1,22 @@
import type { AxiosInstance, AxiosError } from 'axios';
export function errorInterceptor(
instance: AxiosInstance,
onUnauthorized?: () => void,
onError?: (error: unknown) => void
): void {
instance.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
if (error.response?.status === 401 && onUnauthorized) {
onUnauthorized();
}
if (onError) {
onError(error);
}
return Promise.reject(error);
}
);
}

View file

@ -0,0 +1,27 @@
import React, { createContext, useContext, useMemo } from 'react';
import { createApiClient, ApiClient } from './create-api-client';
import type { ApiClientConfig } from './types';
const ApiClientContext = createContext<ApiClient | null>(null);
interface ApiClientProviderProps extends ApiClientConfig {
children: React.ReactNode;
}
export function ApiClientProvider({ children, ...config }: ApiClientProviderProps) {
const client = useMemo(() => createApiClient(config), [config.baseURL]);
return (
<ApiClientContext.Provider value={client}>
{children}
</ApiClientContext.Provider>
);
}
export function useApiClient(): ApiClient {
const client = useContext(ApiClientContext);
if (!client) {
throw new Error('useApiClient must be used within ApiClientProvider');
}
return client;
}

View file

@ -0,0 +1,21 @@
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
export interface ApiClientConfig {
baseURL: string;
timeout?: number;
getAccessToken?: () => string | null;
onUnauthorized?: () => void;
onError?: (error: unknown) => void;
}
export interface RequestConfig extends AxiosRequestConfig {
skipAuth?: boolean;
}
export interface ApiClient {
get<T>(url: string, config?: RequestConfig): Promise<AxiosResponse<T>>;
post<T>(url: string, data?: unknown, config?: RequestConfig): Promise<AxiosResponse<T>>;
put<T>(url: string, data?: unknown, config?: RequestConfig): Promise<AxiosResponse<T>>;
patch<T>(url: string, data?: unknown, config?: RequestConfig): Promise<AxiosResponse<T>>;
delete<T>(url: string, config?: RequestConfig): Promise<AxiosResponse<T>>;
}

View file

@ -8,8 +8,8 @@ describe('Environment Utilities', () => {
describe('getApiUrl', () => {
it('should return API URL from environment or fallback to default', () => {
const url = getApiUrl()
// URL should be valid and contain localhost:4000
expect(url).toMatch(/^https?:\/\/localhost:4000/)
// URL should be valid and contain localhost:4002 (default API port)
expect(url).toMatch(/^https?:\/\/localhost:4002/)
})
})