@prodobit/sdk
Version:
TypeScript SDK for Prodobit API
1 lines • 509 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","names":["status?: number","code?: string","details?: unknown","initialAuthState: AuthState","globalCookiePrefix: string | undefined","domain: string | undefined","cookieOptions: CookieOptions","expiresAt: Date | undefined","tenantId: string | undefined","tokenInfo: any","requestData: RequestOTPRequest","type","verifyData: VerifyOTPRequest","data: CreatePersonRequest","data: CreateOrganizationRequest","type","client: ProdobitClient","defaultCacheConfig: CacheConfig","client: ProdobitClient","errors: ValidationError[]","defaultValidationConfig: ValidationConfig","clientInstance: ProdobitClient | null"],"sources":["../src/types.ts","../src/framework/auth-state.ts","../src/utils/validation.ts","../src/utils/cookie-utils.ts","../src/modules/base-client.ts","../src/modules/auth-client.ts","../src/utils/query-builder.ts","../src/modules/tenant-client.ts","../src/modules/party-client.ts","../src/modules/location-asset-client.ts","../src/modules/manufacturing-client.ts","../src/modules/sales-client.ts","../src/modules/employee-client.ts","../src/modules/inventory-client.ts","../src/modules/purchase-client.ts","../src/modules/brand-client.ts","../src/modules/media-client.ts","../src/modules/warranty-client.ts","../src/modules/assignment-client.ts","../src/modules/asset-purchase-client.ts","../src/modules/department-client.ts","../src/modules/location-type-client.ts","../src/modules/asset-type-client.ts","../src/modules/asset-issue-client.ts","../src/modules/task-client.ts","../src/modules/maintenance-client.ts","../src/modules/calibration-client.ts","../src/modules/service-client.ts","../src/modules/accounting-client.ts","../src/client.ts","../src/framework/api-methods.ts","../src/framework/query-keys.ts","../src/framework/cache-utils.ts","../src/validation/validators.ts","../src/validation/schemas.ts","../src/validation/middleware.ts","../src/singleton.ts","../src/utils/phone-utils.ts"],"sourcesContent":["import { TokenInfo, User } from \"@prodobit/types\";\nimport { type } from \"arktype\";\n\n// Re-export all common types from @prodobit/types as single source of truth\n// (excluding framework types which are defined locally)\nexport type {\n Address,\n ApiErrorResponse,\n ApiPaginatedResponse,\n // Legacy aliases for backward compatibility\n ApiResponse,\n // Asset types\n Asset,\n AssetType,\n // Attribute types\n Attribute,\n AttributeQuery,\n AttributeValue,\n AuthMethod,\n // BOM types\n Bom,\n BomComponent,\n CloneBomRequest,\n ContactMechanism,\n CreateAssetRequest,\n CreateAssetTypeRequest,\n CreateAttributeRequest,\n CreateBomComponentRequest,\n CreateBomRequest,\n CreateEcoRequest,\n // Employee types from types package\n CreateEmployeeRequest,\n CreateItemCategoryRequest,\n CreateItemRequest,\n CreateLocationRequest,\n CreateLocationTypeRequest,\n CreateOrganizationRequest,\n CreatePartyRequest,\n CreatePersonRequest,\n CreateSalesOrderItemRequest,\n CreateSalesOrderRequest,\n CreateTenantRequest,\n Customer,\n // ECO types\n Eco,\n Employee,\n ErrorResponse,\n // Item types\n Item,\n ItemCategory,\n // Enum types\n ItemType,\n // Location types\n Location,\n LocationType,\n LoginResponse,\n LoginResponseData,\n LogoutRequest,\n MrpRequirementsRequest,\n Organization,\n PaginatedResponse,\n Pagination,\n // Party types\n Party,\n PartyRole,\n Person,\n RefreshTokenRequest,\n RejectEcoRequest,\n RequestOTPRequest,\n RequestOTPResponse,\n ResendOTPRequest,\n // Response types - use consistent naming\n Response,\n // Sales types\n SalesOrder,\n SalesOrderItem,\n Session,\n SetAttributeValueRequest,\n Status,\n Supplier,\n // Tenant types\n Tenant,\n TenantMembership,\n TenantQuery,\n Timestamp,\n // Base schemas for reference\n TokenInfo,\n UpdateAssetRequest,\n UpdateAssetTypeRequest,\n UpdateAttributeRequest,\n UpdateBomRequest,\n UpdateEcoRequest,\n UpdateEmployeeRequest,\n UpdateItemCategoryRequest,\n UpdateItemRequest,\n UpdateLocationRequest,\n UpdateLocationTypeRequest,\n UpdatePartyRequest,\n UpdateSalesOrderItemRequest,\n UpdateSalesOrderRequest,\n UpdateSalesOrderStatusRequest,\n UpdateTenantRequest,\n // Auth types\n User,\n // Core types\n UUID,\n VerifyOTPRequest,\n} from \"@prodobit/types\";\n\n// SDK-specific arktype schemas\nexport const prodobitClientConfig = type({\n baseUrl: \"string.url\",\n \"apiKey?\": \"string\",\n \"timeout?\": \"number >= 0\",\n \"headers?\": \"object\",\n \"autoRefresh?\": \"boolean\",\n \"persistToken?\": \"boolean\",\n \"tokenStorageKey?\": \"string\",\n \"cookiePrefix?\": \"string\",\n});\n\nexport const requestConfig = type({\n \"headers?\": \"object\",\n \"timeout?\": \"number >= 0\",\n \"skipAuth?\": \"boolean\",\n});\n\nexport const sdkTokenInfo = type({\n accessToken: \"string\",\n \"refreshToken?\": \"string\",\n expiresAt: \"Date\",\n \"tenantId?\": \"string\",\n});\n\n// Query Filter Schemas\nexport const salesOrderQuery = type({\n \"status?\": \"string\",\n \"customerId?\": \"string\",\n \"orderDateFrom?\": \"string\",\n \"orderDateTo?\": \"string\",\n \"search?\": \"string\",\n \"page?\": \"number >= 1\",\n \"limit?\": \"number >= 1\",\n});\n\nexport const salesOrderFilters = salesOrderQuery;\n\nexport const itemFilters = type({\n \"search?\": \"string\",\n \"type?\": \"'product' | 'service' | 'raw_material' | 'component'\",\n \"status?\": \"'active' | 'inactive' | 'suspended' | 'deleted'\",\n \"categoryId?\": \"string\",\n \"page?\": \"number >= 1\",\n \"limit?\": \"number >= 1\",\n});\n\nexport const locationFilters = type({\n \"search?\": \"string\",\n \"type?\": \"string\",\n \"parentId?\": \"string\",\n \"status?\": \"'active' | 'inactive' | 'suspended' | 'deleted'\",\n \"page?\": \"number >= 1\",\n \"limit?\": \"number >= 1\",\n});\n\nexport const assetFilters = type({\n \"search?\": \"string\",\n \"type?\": \"string\",\n \"locationId?\": \"string\",\n \"status?\": \"'active' | 'inactive' | 'suspended' | 'deleted'\",\n \"page?\": \"number >= 1\",\n \"limit?\": \"number >= 1\",\n});\n\nexport const partyFilters = type({\n \"search?\": \"string\",\n \"type?\": \"'person' | 'organization'\",\n \"role?\": \"'customer' | 'supplier' | 'employee'\",\n \"status?\": \"'active' | 'inactive' | 'suspended' | 'deleted'\",\n \"page?\": \"number >= 1\",\n \"limit?\": \"number >= 1\",\n});\n\nexport const bomFilters = type({\n \"search?\": \"string\",\n \"itemId?\": \"string\",\n \"status?\": \"'draft' | 'approved' | 'suspended' | 'obsolete'\",\n \"page?\": \"number >= 1\",\n \"limit?\": \"number >= 1\",\n});\n\nexport const bomComponentFilters = type({\n \"bomId?\": \"string\",\n \"itemId?\": \"string\",\n \"page?\": \"number >= 1\",\n \"limit?\": \"number >= 1\",\n});\n\nexport const ecoFilters = type({\n \"search?\": \"string\",\n \"status?\": \"'draft' | 'in_review' | 'approved' | 'rejected' | 'implemented'\",\n \"bomId?\": \"string\",\n \"priority?\": \"'low' | 'medium' | 'high' | 'critical'\",\n \"page?\": \"number >= 1\",\n \"limit?\": \"number >= 1\",\n});\n\nexport const lotFilters = type({\n \"search?\": \"string\",\n \"itemId?\": \"string\",\n \"status?\": \"'active' | 'inactive' | 'suspended' | 'deleted'\",\n \"page?\": \"number >= 1\",\n \"limit?\": \"number >= 1\",\n});\n\nexport const stockFilters = type({\n \"itemId?\": \"string\",\n \"locationId?\": \"string\",\n \"lotId?\": \"string\",\n \"status?\": \"'available' | 'reserved' | 'quarantined' | 'damaged' | 'blocked'\",\n \"page?\": \"number >= 1\",\n \"limit?\": \"number >= 1\",\n});\n\nexport const stockTransactionFilters = type({\n \"itemId?\": \"string\",\n \"locationId?\": \"string\",\n \"lotId?\": \"string\",\n \"transactionType?\": \"string\",\n \"dateFrom?\": \"string\",\n \"dateTo?\": \"string\",\n \"page?\": \"number >= 1\",\n \"limit?\": \"number >= 1\",\n});\n\n// Framework API method filter schemas\nexport const frameworkPartyFilters = partyFilters;\n\nexport const frameworkSearchParams = type({\n searchTerm: \"string\",\n \"partyType?\": \"'person' | 'organization'\",\n \"roleType?\": \"'customer' | 'supplier' | 'employee'\",\n});\n\nexport const frameworkEmployeeFilters = type({\n \"department?\": \"string\",\n \"role?\": \"string\",\n \"status?\": \"'active' | 'inactive' | 'on_leave'\",\n \"search?\": \"string\",\n \"page?\": \"number >= 1\",\n \"limit?\": \"number >= 1\",\n});\n\nexport const frameworkCustomerFilters = type({\n \"status?\": \"'active' | 'inactive' | 'suspended'\",\n \"customerType?\": \"'retail' | 'wholesale' | 'vip'\",\n \"region?\": \"string\",\n \"search?\": \"string\",\n \"page?\": \"number >= 1\",\n \"limit?\": \"number >= 1\",\n});\n\nexport const frameworkSupplierFilters = type({\n \"status?\": \"'active' | 'inactive' | 'suspended'\",\n \"category?\": \"string\",\n \"rating?\": \"number\",\n \"search?\": \"string\",\n \"page?\": \"number >= 1\",\n \"limit?\": \"number >= 1\",\n});\n\nexport const frameworkStockFilters = type({\n \"itemId?\": \"string\",\n \"locationId?\": \"string\",\n \"lotId?\": \"string\",\n \"status?\": \"'available' | 'reserved' | 'quarantined' | 'damaged' | 'blocked'\",\n \"lowStock?\": \"boolean\",\n \"search?\": \"string\",\n \"page?\": \"number >= 1\",\n \"limit?\": \"number >= 1\",\n});\n\nexport const frameworkLotFilters = lotFilters;\n\n// BOM specific filters\nexport const frameworkBomFilters = type({\n \"itemId?\": \"string\",\n \"status?\": \"'draft' | 'active' | 'obsolete'\",\n \"version?\": \"string\",\n \"effectiveDate?\": \"string.date\",\n \"search?\": \"string\",\n \"page?\": \"number >= 1\",\n \"limit?\": \"number >= 1\",\n});\n\nexport const frameworkBomComponentFilters = type({\n \"bomId?\": \"string\",\n \"componentId?\": \"string\",\n \"componentType?\": \"string\",\n \"required?\": \"boolean\",\n \"page?\": \"number >= 1\",\n \"limit?\": \"number >= 1\",\n});\n\n// Additional schemas for missing types\nexport const contactInfo = type({\n type: \"'email' | 'phone'\",\n value: \"string\",\n \"isPrimary?\": \"boolean\",\n});\n\nexport const addressInfo = type({\n type: \"'billing' | 'shipping' | 'home'\",\n line1: \"string\",\n \"line2?\": \"string\",\n city: \"string\",\n state: \"string\",\n postalCode: \"string\",\n country: \"string\",\n});\n\nexport const createInvitationRequest = type({\n email: \"string.email\",\n roleId: \"string.uuid\",\n \"message?\": \"string\",\n \"expiresInDays?\": \"number >= 1\",\n \"membershipExpiresAt?\": \"string.date\",\n \"accessLevel?\": \"'full' | 'limited' | 'read_only'\",\n \"permissions?\": \"object\",\n \"resourceRestrictions?\": \"object\",\n});\n\nexport const updateMembershipRequest = type({\n \"role?\": \"string\",\n \"permissions?\": \"string[]\",\n \"status?\": \"'active' | 'inactive' | 'suspended'\",\n});\n\nexport const createStockRequest = type({\n itemId: \"string.uuid\",\n locationId: \"string.uuid\",\n \"lotId?\": \"string.uuid\",\n quantity: \"number >= 0\",\n \"unitCost?\": \"number >= 0\",\n status: \"'available' | 'reserved' | 'quarantined' | 'damaged' | 'blocked'\",\n});\n\nexport const updateStockRequest = type({\n \"quantity?\": \"number >= 0\",\n \"unitCost?\": \"number >= 0\",\n \"status?\": \"'available' | 'reserved' | 'quarantined' | 'damaged' | 'blocked'\",\n});\n\n// Note: Employee request types are now imported from @prodobit/types\n\n// Lot request types\nexport const createLotRequest = type({\n itemId: \"string.uuid\",\n lotNumber: \"string >= 1\",\n \"expirationDate?\": \"string.date\",\n \"notes?\": \"string\",\n});\n\nexport const updateLotRequest = type({\n \"lotNumber?\": \"string >= 1\",\n \"expirationDate?\": \"string.date\",\n \"notes?\": \"string\",\n});\n\n// Query utility types\nexport const queryPrimitive = type(\n \"string | number | boolean | Date | null | undefined\"\n);\nexport const queryValue = queryPrimitive.or(queryPrimitive.array());\n\n// SDK-specific enum types\nexport const partyTypeEnum = type(\"'person' | 'organization'\");\nexport const roleTypeEnum = type(\"'customer' | 'supplier' | 'employee'\");\n\n// Framework-specific arktype schemas (moved from @prodobit/types)\n// Auth State Schema\nexport const authState = type({\n isAuthenticated: \"boolean\",\n isLoading: \"boolean\",\n isError: \"boolean\",\n \"user?\": \"object | null\",\n \"token?\": \"object | null\",\n \"error?\": \"object | null\",\n \"tenantId?\": \"string | null\",\n});\n\n// Cache Configuration Schema\nexport const cacheConfig = type({\n defaultStaleTime: \"number >= 0\",\n defaultCacheTime: \"number >= 0\",\n \"resourceStaleTime?\": {\n \"auth?\": \"number >= 0\",\n \"tenants?\": \"number >= 0\",\n \"parties?\": \"number >= 0\",\n \"items?\": \"number >= 0\",\n \"locations?\": \"number >= 0\",\n \"assets?\": \"number >= 0\",\n \"stocks?\": \"number >= 0\",\n \"lots?\": \"number >= 0\",\n \"boms?\": \"number >= 0\",\n \"salesOrders?\": \"number >= 0\",\n \"employees?\": \"number >= 0\",\n \"attributes?\": \"number >= 0\",\n \"ecos?\": \"number >= 0\",\n },\n refetchOnWindowFocus: \"boolean\",\n refetchOnReconnect: \"boolean\",\n refetchInterval: \"number >= 0 | false\",\n retry: \"number >= 0 | boolean\",\n retryDelay: \"number >= 0\",\n});\n\n// Auth Actions Schema - TypeScript union types (arktype çok karmaşık oldu)\nexport type AuthAction =\n | { type: \"AUTH_START\" }\n | {\n type: \"AUTH_SUCCESS\";\n payload: {\n user: User; // User from @prodobit/types\n token: TokenInfo; // TokenInfo from @prodobit/types\n };\n }\n | {\n type: \"AUTH_ERROR\";\n payload: { error: string | Error | ProdobitError };\n }\n | { type: \"AUTH_LOGOUT\" }\n | {\n type: \"TOKEN_REFRESH\";\n payload: { token: TokenInfo }; // TokenInfo from @prodobit/types\n }\n | {\n type: \"SET_TENANT\";\n payload: { tenantId: string };\n }\n | { type: \"CLEAR_ERROR\" };\n\n// Type exports from arktype schemas\nexport type ProdobitClientConfig = typeof prodobitClientConfig.infer;\nexport type RequestConfig = typeof requestConfig.infer;\nexport type SDKTokenInfo = typeof sdkTokenInfo.infer;\nexport type SalesOrderQuery = typeof salesOrderQuery.infer;\n\n// Filter type exports\nexport type ItemFilters = typeof itemFilters.infer;\nexport type LocationFilters = typeof locationFilters.infer;\nexport type AssetFilters = typeof assetFilters.infer;\nexport type PartyFilters = typeof partyFilters.infer;\nexport type SalesOrderFilters = typeof salesOrderFilters.infer;\nexport type BomFilters = typeof bomFilters.infer;\nexport type BomComponentFilters = typeof bomComponentFilters.infer;\nexport type EcoFilters = typeof ecoFilters.infer;\nexport type LotFilters = typeof lotFilters.infer;\nexport type StockFilters = typeof stockFilters.infer;\nexport type StockTransactionFilters = typeof stockTransactionFilters.infer;\n\n// Framework API filter type exports\nexport type FrameworkPartyFilters = typeof frameworkPartyFilters.infer;\nexport type FrameworkSearchParams = typeof frameworkSearchParams.infer;\nexport type FrameworkEmployeeFilters = typeof frameworkEmployeeFilters.infer;\nexport type FrameworkCustomerFilters = typeof frameworkCustomerFilters.infer;\nexport type FrameworkSupplierFilters = typeof frameworkSupplierFilters.infer;\nexport type FrameworkStockFilters = typeof frameworkStockFilters.infer;\nexport type FrameworkLotFilters = typeof frameworkLotFilters.infer;\nexport type FrameworkBomFilters = typeof frameworkBomFilters.infer;\nexport type FrameworkBomComponentFilters =\n typeof frameworkBomComponentFilters.infer;\n\n// Additional type exports\nexport type ContactInfo = typeof contactInfo.infer;\nexport type AddressInfo = typeof addressInfo.infer;\nexport type CreateInvitationRequest = typeof createInvitationRequest.infer;\nexport type UpdateMembershipRequest = typeof updateMembershipRequest.infer;\nexport type CreateStockRequest = typeof createStockRequest.infer;\nexport type UpdateStockRequest = typeof updateStockRequest.infer;\n// Note: Employee request types are exported from @prodobit/types\nexport type CreateLotRequest = typeof createLotRequest.infer;\nexport type UpdateLotRequest = typeof updateLotRequest.infer;\n\n// Base type aliases for backward compatibility\nexport type AssetBase = any; // Using Asset from @prodobit/types\nexport type ItemBase = any; // Using Item from @prodobit/types\nexport type LocationBase = any; // Using Location from @prodobit/types\nexport type LotBase = any; // Lot type not yet defined in types package\nexport type StockBase = any; // Stock type not yet defined in types package\n\nexport type QueryPrimitive = typeof queryPrimitive.infer;\nexport type QueryValue = typeof queryValue.infer;\nexport type PartyType = typeof partyTypeEnum.infer;\nexport type RoleType = typeof roleTypeEnum.infer;\n\n// Framework type exports\nexport type AuthState = typeof authState.infer;\nexport type CacheConfig = typeof cacheConfig.infer;\n\n// SDK-specific error class\nexport class ProdobitError extends Error {\n constructor(\n message: string,\n public readonly status?: number,\n public readonly code?: string,\n public readonly details?: unknown\n ) {\n super(message);\n this.name = \"ProdobitError\";\n }\n\n // Factory methods for common error types\n static badRequest(message: string, details?: unknown): ProdobitError {\n return new ProdobitError(message, 400, \"BAD_REQUEST\", details);\n }\n\n static unauthorized(message = \"Unauthorized\"): ProdobitError {\n return new ProdobitError(message, 401, \"UNAUTHORIZED\");\n }\n\n static forbidden(message = \"Forbidden\"): ProdobitError {\n return new ProdobitError(message, 403, \"FORBIDDEN\");\n }\n\n static notFound(resource: string, id?: string): ProdobitError {\n const message = id\n ? `${resource} with ID '${id}' not found`\n : `${resource} not found`;\n return new ProdobitError(message, 404, \"NOT_FOUND\", { resource, id });\n }\n\n static conflict(message: string, details?: unknown): ProdobitError {\n return new ProdobitError(message, 409, \"CONFLICT\", details);\n }\n\n static validationError(message: string, details?: unknown): ProdobitError {\n return new ProdobitError(message, 422, \"VALIDATION_ERROR\", details);\n }\n\n static serverError(\n message = \"Internal Server Error\",\n details?: unknown\n ): ProdobitError {\n return new ProdobitError(message, 500, \"INTERNAL_ERROR\", details);\n }\n\n static networkError(message = \"Network Error\"): ProdobitError {\n return new ProdobitError(message, 0, \"NETWORK_ERROR\");\n }\n\n static timeout(message = \"Request Timeout\"): ProdobitError {\n return new ProdobitError(message, 408, \"TIMEOUT\");\n }\n\n // Check error type\n isNetworkError(): boolean {\n return this.code === \"NETWORK_ERROR\";\n }\n\n isAuthError(): boolean {\n return this.status === 401 || this.status === 403;\n }\n\n isValidationError(): boolean {\n return this.code === \"VALIDATION_ERROR\";\n }\n\n isNotFoundError(): boolean {\n return this.status === 404;\n }\n}\n\n// All types are now defined via arktype schemas above\n","// Authentication State Management Helpers\n\nimport type { TokenInfo, User } from \"@prodobit/types\";\nimport type { ProdobitClient } from \"../client\";\nimport type { AuthAction, AuthState } from \"../types\";\nimport { ProdobitError } from \"../types\";\n\n/**\n * Initial authentication state\n */\nexport const initialAuthState: AuthState = {\n isAuthenticated: false,\n isLoading: false,\n isError: false,\n user: null,\n token: null,\n error: null,\n tenantId: null,\n};\n\n/**\n * Authentication State Reducer\n */\nexport function authReducer(state: AuthState, action: AuthAction): AuthState {\n switch (action.type) {\n case \"AUTH_START\":\n return {\n ...state,\n isLoading: true,\n isError: false,\n error: null,\n };\n\n case \"AUTH_SUCCESS\":\n return {\n ...state,\n isLoading: false,\n isError: false,\n isAuthenticated: true,\n user: action.payload.user,\n token: action.payload.token,\n tenantId: action.payload.token.tenantId || null,\n error: null,\n };\n\n case \"AUTH_ERROR\":\n return {\n ...state,\n isLoading: false,\n isError: true,\n isAuthenticated: false,\n user: null,\n token: null,\n tenantId: null,\n error:\n typeof action.payload.error === \"string\"\n ? new Error(action.payload.error)\n : action.payload.error,\n };\n\n case \"AUTH_LOGOUT\":\n return {\n ...initialAuthState,\n };\n\n case \"TOKEN_REFRESH\":\n return {\n ...state,\n token: action.payload.token,\n tenantId: action.payload.token.tenantId || state.tenantId,\n error: (state.error as any)?.isAuthError?.() ? null : state.error, // Clear auth errors on successful refresh\n };\n\n case \"SET_TENANT\":\n return {\n ...state,\n tenantId: action.payload.tenantId,\n };\n\n case \"CLEAR_ERROR\":\n return {\n ...state,\n isError: false,\n error: null,\n };\n\n default:\n return state;\n }\n}\n\n/**\n * Authentication State Manager\n * Provides a framework-agnostic way to manage authentication state\n */\nexport class AuthStateManager {\n private state: AuthState = initialAuthState;\n private listeners = new Set<(state: AuthState) => void>();\n private client: ProdobitClient;\n private refreshTimer: ReturnType<typeof setTimeout> | null = null;\n private isInitialized = false;\n\n constructor(client: ProdobitClient) {\n this.client = client;\n \n // If we have a token in sessionStorage, start in loading state\n const existingToken = this.client.getTokenInfo();\n if (existingToken) {\n this.state = {\n ...initialAuthState,\n isLoading: true,\n };\n }\n \n this.setupAutoRefresh();\n }\n\n /**\n * Subscribe to auth state changes\n */\n subscribe(listener: (state: AuthState) => void): () => void {\n this.listeners.add(listener);\n // Immediately call with current state\n listener(this.state);\n\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Get current auth state\n */\n getState(): AuthState {\n return this.state;\n }\n\n /**\n * Update state and notify listeners\n */\n private setState(action: AuthAction): void {\n const newState = authReducer(this.state, action);\n const hasChanged = newState !== this.state;\n\n this.state = newState;\n\n if (hasChanged) {\n this.listeners.forEach((listener) => listener(this.state));\n }\n }\n\n /**\n * Initialize authentication from stored token\n */\n async initialize(): Promise<void> {\n // Prevent multiple initializations\n if (this.isInitialized) {\n return;\n }\n this.isInitialized = true;\n\n // Try to get current user to check if we have valid session\n try {\n this.setState({ type: \"AUTH_START\" });\n \n // Check if we have a valid token first\n const currentToken = this.client.getTokenInfo();\n const isTokenValid = this.client.isTokenValid();\n \n // Only refresh if we don't have a valid token AND have refresh token\n if (!currentToken || !isTokenValid) {\n if (currentToken?.refreshToken) {\n try {\n console.log('Attempting token refresh during initialization');\n await this.client.refreshToken();\n } catch (error) {\n console.log('Refresh failed during initialization:', error);\n // If refresh fails, we're not authenticated\n this.setState({ type: \"AUTH_LOGOUT\" });\n return;\n }\n } else {\n console.log('No refresh token available, clearing auth state');\n // No refresh token, clear auth state\n this.setState({ type: \"AUTH_LOGOUT\" });\n return;\n }\n }\n\n // Get current user info\n const userResponse = await this.client.getCurrentUser();\n\n if (userResponse.success && userResponse.data) {\n this.setState({\n type: \"AUTH_SUCCESS\",\n payload: {\n user: userResponse.data.user,\n token: this.client.getTokenInfo()!,\n },\n });\n } else {\n throw ProdobitError.unauthorized(\"Failed to get user info\");\n }\n } catch (error) {\n console.log('Auth initialization failed:', error);\n this.setState({\n type: \"AUTH_ERROR\",\n payload: {\n error:\n error instanceof ProdobitError\n ? error\n : ProdobitError.serverError(\n \"Authentication initialization failed\"\n ),\n },\n });\n }\n }\n\n /**\n * Login with OTP\n */\n async loginWithOTP(\n email: string,\n tenantId?: string\n ): Promise<{ success: boolean; expiresAt?: string; error?: string }> {\n try {\n this.setState({ type: \"AUTH_START\" });\n\n const response = await this.client.requestOTP({ email, tenantId });\n\n if (response.success) {\n return {\n success: true,\n expiresAt: response.expiresAt,\n };\n } else {\n const error = ProdobitError.badRequest(\"Failed to send OTP\");\n this.setState({ type: \"AUTH_ERROR\", payload: { error } });\n return {\n success: false,\n error: error.message,\n };\n }\n } catch (error) {\n const authError =\n error instanceof ProdobitError\n ? error\n : ProdobitError.serverError(\"OTP request failed\");\n\n this.setState({ type: \"AUTH_ERROR\", payload: { error: authError } });\n return {\n success: false,\n error: authError.message,\n };\n }\n }\n\n /**\n * Verify OTP and complete login\n */\n async verifyOTP(\n email: string,\n code: string,\n tenantId?: string\n ): Promise<{ success: boolean; user?: User; error?: string }> {\n try {\n this.setState({ type: \"AUTH_START\" });\n\n const response = await this.client.verifyOTP({ email, code, tenantId });\n\n if (response.success && response.data) {\n const token = this.client.getTokenInfo();\n if (token) {\n this.setState({\n type: \"AUTH_SUCCESS\",\n payload: {\n user: response.data.user,\n token,\n },\n });\n\n return {\n success: true,\n user: response.data.user,\n };\n }\n }\n\n const error = ProdobitError.unauthorized(\"OTP verification failed\");\n this.setState({ type: \"AUTH_ERROR\", payload: { error } });\n return {\n success: false,\n error: error.message,\n };\n } catch (error) {\n const authError =\n error instanceof ProdobitError\n ? error\n : ProdobitError.unauthorized(\"OTP verification failed\");\n\n this.setState({ type: \"AUTH_ERROR\", payload: { error: authError } });\n return {\n success: false,\n error: authError.message,\n };\n }\n }\n\n /**\n * Refresh authentication token\n */\n async refreshToken(): Promise<void> {\n try {\n const response = await this.client.refreshToken();\n\n if (response.success && response.data) {\n const token = this.client.getTokenInfo();\n if (token) {\n this.setState({\n type: \"TOKEN_REFRESH\",\n payload: { token },\n });\n this.setupAutoRefresh(); // Reset refresh timer\n }\n } else {\n throw ProdobitError.unauthorized(\"Token refresh failed\");\n }\n } catch (error) {\n const authError =\n error instanceof ProdobitError\n ? error\n : ProdobitError.unauthorized(\"Token refresh failed\");\n\n this.setState({ type: \"AUTH_ERROR\", payload: { error: authError } });\n throw authError;\n }\n }\n\n /**\n * Logout user\n */\n async logout(allDevices = false): Promise<void> {\n try {\n await this.client.logout({ allDevices });\n } catch (error) {\n // Log error but still clear local state\n console.warn(\"Logout API call failed:\", error);\n } finally {\n this.clearRefreshTimer();\n this.setState({ type: \"AUTH_LOGOUT\" });\n }\n }\n\n /**\n * Set current tenant\n */\n setTenant(tenantId: string): void {\n this.setState({\n type: \"SET_TENANT\",\n payload: { tenantId },\n });\n }\n\n /**\n * Clear authentication error\n */\n clearError(): void {\n this.setState({ type: \"CLEAR_ERROR\" });\n }\n\n /**\n * Setup automatic token refresh\n *\n * NOTE: Disabled in favor of BaseClient's per-request refresh mechanism\n * BaseClient automatically refreshes tokens before each request when needed\n */\n private setupAutoRefresh(): void {\n // Disabled - BaseClient handles refresh automatically\n this.clearRefreshTimer();\n\n // If you want to re-enable timer-based refresh, uncomment below:\n /*\n const token = this.client.getTokenInfo();\n if (!token) return;\n\n // Refresh 5 minutes before expiration\n const refreshTime = token.expiresAt.getTime() - Date.now() - 5 * 60 * 1000;\n\n if (refreshTime > 0) {\n this.refreshTimer = setTimeout(async () => {\n try {\n await this.refreshToken();\n } catch (error) {\n console.warn(\"Automatic token refresh failed:\", error);\n // If refresh fails, logout user to force re-authentication\n this.setState({ type: \"AUTH_LOGOUT\" });\n }\n }, refreshTime);\n }\n */\n }\n\n /**\n * Clear refresh timer\n */\n private clearRefreshTimer(): void {\n if (this.refreshTimer) {\n clearTimeout(this.refreshTimer);\n this.refreshTimer = null;\n }\n }\n\n /**\n * Cleanup resources\n */\n destroy(): void {\n this.clearRefreshTimer();\n this.listeners.clear();\n }\n}\n\n/**\n * Authentication helpers for different frameworks\n */\nexport const authHelpers = {\n /**\n * React hooks compatible state selector\n */\n createStateSelector:\n <T>(selector: (state: AuthState) => T) =>\n (state: AuthState): T =>\n selector(state),\n\n /**\n * Common state selectors\n */\n selectors: {\n isAuthenticated: (state: AuthState) => state.isAuthenticated,\n isLoading: (state: AuthState) => state.isLoading,\n isError: (state: AuthState) => state.isError,\n user: (state: AuthState) => state.user,\n token: (state: AuthState) => state.token,\n error: (state: AuthState) => state.error,\n tenantId: (state: AuthState) => state.tenantId,\n\n // Derived state\n isReady: (state: AuthState) => !state.isLoading && !state.isError,\n hasUser: (state: AuthState) => state.isAuthenticated && !!state.user,\n hasTenant: (state: AuthState) => !!state.tenantId,\n\n // Error type checks\n hasAuthError: (state: AuthState) => !!(state.error as any)?.isAuthError?.(),\n hasNetworkError: (state: AuthState) =>\n !!(state.error as any)?.isNetworkError?.(),\n hasValidationError: (state: AuthState) =>\n !!(state.error as any)?.isValidationError?.(),\n },\n\n /**\n * Action creators for external state management\n */\n actions: {\n startAuth: (): AuthAction => ({ type: \"AUTH_START\" }),\n authSuccess: (user: User, token: TokenInfo): AuthAction => ({\n type: \"AUTH_SUCCESS\",\n payload: { user, token },\n }),\n authError: (error: ProdobitError): AuthAction => ({\n type: \"AUTH_ERROR\",\n payload: { error },\n }),\n logout: (): AuthAction => ({ type: \"AUTH_LOGOUT\" }),\n refreshToken: (token: TokenInfo): AuthAction => ({\n type: \"TOKEN_REFRESH\",\n payload: { token },\n }),\n setTenant: (tenantId: string): AuthAction => ({\n type: \"SET_TENANT\",\n payload: { tenantId },\n }),\n clearError: (): AuthAction => ({ type: \"CLEAR_ERROR\" }),\n },\n};\n\n/**\n * Token management utilities\n */\nexport const tokenUtils = {\n /**\n * Check if token is expiring soon\n */\n isExpiringSoon: (token: TokenInfo, thresholdMinutes = 5): boolean => {\n const threshold = thresholdMinutes * 60 * 1000;\n return token.expiresAt.getTime() - Date.now() < threshold;\n },\n\n /**\n * Get time until expiration\n */\n getTimeUntilExpiration: (token: TokenInfo): number => {\n return Math.max(0, token.expiresAt.getTime() - Date.now());\n },\n\n /**\n * Format expiration time\n */\n formatExpiration: (token: TokenInfo): string => {\n const timeLeft = tokenUtils.getTimeUntilExpiration(token);\n const minutes = Math.floor(timeLeft / (60 * 1000));\n const hours = Math.floor(minutes / 60);\n\n if (hours > 0) {\n return `${hours}h ${minutes % 60}m`;\n }\n return `${minutes}m`;\n },\n\n /**\n * Decode token payload (without verification)\n */\n decodeTokenPayload: (token: string): any | null => {\n try {\n const payload = token.split(\".\")[1];\n return JSON.parse(atob(payload));\n } catch {\n return null;\n }\n },\n};\n","import { type } from \"arktype\";\nimport { ProdobitError } from \"../types\";\n\nexport function validateRequest<T extends Record<string, any>>(\n schema: (data: unknown) => T | type.errors,\n data: unknown,\n errorMessage = \"Invalid request data\"\n): T {\n const result = schema(data);\n \n if (result instanceof type.errors) {\n throw ProdobitError.validationError(errorMessage, result.summary);\n }\n \n return result;\n}\n\nexport function validateResponse<T extends Record<string, any>>(\n schema: (data: unknown) => T | type.errors,\n data: unknown,\n errorMessage = \"Invalid response data\"\n): T {\n const result = schema(data);\n \n if (result instanceof type.errors) {\n throw ProdobitError.serverError(errorMessage, result.summary);\n }\n \n return result;\n}","// Cookie management utilities for auth tokens\nimport type { TokenInfo } from \"../types\";\n\nexport interface CookieOptions {\n maxAge?: number; // in seconds\n expires?: Date;\n domain?: string;\n path?: string;\n secure?: boolean;\n sameSite?: 'strict' | 'lax' | 'none';\n httpOnly?: boolean;\n}\n\n/**\n * Cookie utilities for token management\n */\nexport const cookieUtils = {\n /**\n * Set a cookie with proper options\n */\n set(name: string, value: string, options: CookieOptions = {}): void {\n if (typeof document === 'undefined') return; // SSR guard\n\n const {\n maxAge,\n expires,\n domain,\n path = '/',\n secure,\n sameSite = 'lax',\n httpOnly = false\n } = options;\n\n let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;\n\n if (maxAge !== undefined) {\n cookieString += `; Max-Age=${maxAge}`;\n }\n\n if (expires) {\n cookieString += `; Expires=${expires.toUTCString()}`;\n }\n\n if (domain) {\n cookieString += `; Domain=${domain}`;\n }\n\n cookieString += `; Path=${path}`;\n\n if (secure) {\n cookieString += '; Secure';\n }\n\n cookieString += `; SameSite=${sameSite}`;\n\n if (httpOnly) {\n cookieString += '; HttpOnly';\n }\n\n document.cookie = cookieString;\n },\n\n /**\n * Get a cookie value by name\n */\n get(name: string): string | undefined {\n if (typeof document === 'undefined') return undefined; // SSR guard\n\n const nameEQ = encodeURIComponent(name) + '=';\n const cookies = document.cookie.split(';');\n\n for (let cookie of cookies) {\n cookie = cookie.trim();\n if (cookie.startsWith(nameEQ)) {\n return decodeURIComponent(cookie.substring(nameEQ.length));\n }\n }\n\n return undefined;\n },\n\n /**\n * Remove a cookie\n */\n remove(name: string, options: Pick<CookieOptions, 'domain' | 'path'> = {}): void {\n this.set(name, '', {\n ...options,\n maxAge: 0,\n expires: new Date(0)\n });\n },\n\n /**\n * Check if a cookie exists\n */\n exists(name: string): boolean {\n return this.get(name) !== undefined;\n }\n};\n\n// Global cookie prefix store\nlet globalCookiePrefix: string | undefined;\n\n/**\n * Set global cookie prefix (called from client initialization)\n */\nexport function setCookiePrefix(prefix: string | undefined): void {\n globalCookiePrefix = prefix;\n}\n\n/**\n * Get cookie prefix from multiple sources in priority order:\n * 1. Global prefix (set via setCookiePrefix from client config)\n * 2. Environment variable\n * 3. Window object (browser only)\n * Defaults to 'prodobit' if not set\n */\nfunction getCookiePrefix(): string {\n // First priority: global prefix from client config\n if (globalCookiePrefix) {\n return globalCookiePrefix;\n }\n // Second priority: environment variable\n if (typeof process !== 'undefined' && process.env?.COOKIE_PREFIX) {\n return process.env.COOKIE_PREFIX;\n }\n // Third priority: window object (browser)\n if (typeof window !== 'undefined' && (window as any).__COOKIE_PREFIX__) {\n return (window as any).__COOKIE_PREFIX__;\n }\n return 'prodobit';\n}\n\n/**\n * Generate cookie name with dynamic prefix\n */\nfunction getCookieName(name: string): string {\n return `${getCookiePrefix()}_${name}`;\n}\n\n/**\n * Token-specific cookie management\n */\nexport const tokenCookies = {\n // Cookie names - now dynamic\n get ACCESS_TOKEN() { return getCookieName('access_token'); },\n get REFRESH_TOKEN() { return getCookieName('refresh_token'); },\n get CSRF_TOKEN() { return getCookieName('csrf_token'); }, // This will be HTTP-only from server\n get TENANT_ID() { return getCookieName('tenant_id'); },\n\n /**\n * Store auth tokens in cookies\n * - Access token: accessible cookie (for React to read auth state)\n * - Refresh token: accessible cookie (for client-side refresh)\n * - CSRF token: HTTP-only (set by server, used in headers)\n */\n setTokens(tokenInfo: TokenInfo): void {\n // Determine production environment and domain from current hostname\n const hostname = typeof window !== 'undefined' ? window.location.hostname : '';\n const isProduction = process.env.NODE_ENV === 'production' ||\n (hostname && !hostname.includes('localhost'));\n\n // Extract root domain for cookie sharing across subdomains\n // e.g., app.assetspotter.com -> .assetspotter.com\n let domain: string | undefined;\n if (isProduction && hostname) {\n const parts = hostname.split('.');\n if (parts.length >= 2) {\n // Get last two parts (e.g., assetspotter.com)\n domain = '.' + parts.slice(-2).join('.');\n }\n }\n\n const cookieOptions: CookieOptions = {\n path: '/',\n secure: isProduction || false,\n sameSite: 'lax',\n httpOnly: false, // These need to be accessible to JS\n domain: domain\n };\n\n // Calculate maxAge from expiresAt\n const maxAge = tokenInfo.expiresAt \n ? Math.max(0, Math.floor((tokenInfo.expiresAt.getTime() - Date.now()) / 1000))\n : 60 * 60 * 24; // 24 hours default\n\n // Set access token (accessible)\n if (tokenInfo.accessToken) {\n cookieUtils.set(this.ACCESS_TOKEN, tokenInfo.accessToken, {\n ...cookieOptions,\n maxAge\n });\n }\n\n // Set refresh token (accessible) - longer expiry\n if (tokenInfo.refreshToken) {\n cookieUtils.set(this.REFRESH_TOKEN, tokenInfo.refreshToken, {\n ...cookieOptions,\n maxAge: 60 * 60 * 24 * 30 // 30 days\n });\n }\n\n // Set tenant ID if available\n if (tokenInfo.tenantId) {\n cookieUtils.set(this.TENANT_ID, tokenInfo.tenantId, {\n ...cookieOptions,\n maxAge: 60 * 60 * 24 * 30 // 30 days\n });\n }\n\n // NOTE: CSRF token will be set by server as HTTP-only cookie\n // We don't set it here, but we'll read it from server responses\n },\n\n /**\n * Get token info from cookies\n * Returns token info if either access token OR refresh token exists\n * This allows token refresh even after access token cookie expires\n */\n getTokens(): TokenInfo | undefined {\n const accessToken = cookieUtils.get(this.ACCESS_TOKEN);\n const refreshToken = cookieUtils.get(this.REFRESH_TOKEN);\n const tenantId = cookieUtils.get(this.TENANT_ID);\n\n // If neither token exists, return undefined\n if (!accessToken && !refreshToken) {\n return undefined;\n }\n\n // Try to decode expiration from access token\n let expiresAt: Date | undefined;\n if (accessToken) {\n try {\n const payload = JSON.parse(atob(accessToken.split('.')[1]));\n if (payload.exp) {\n expiresAt = new Date(payload.exp * 1000);\n }\n } catch {\n // If we can't decode, token might be expired/invalid\n expiresAt = new Date(0); // Mark as expired to trigger refresh\n }\n } else {\n // No access token but have refresh token - mark as expired to trigger refresh\n expiresAt = new Date(0);\n }\n\n return {\n accessToken: accessToken || '', // Empty string if no access token\n refreshToken,\n expiresAt: expiresAt || new Date(0),\n tenantId,\n } as TokenInfo;\n },\n\n /**\n * Clear all auth cookies\n */\n clearTokens(): void {\n // Determine production environment and domain from current hostname\n const hostname = typeof window !== 'undefined' ? window.location.hostname : '';\n const isProduction = process.env.NODE_ENV === 'production' ||\n (hostname && !hostname.includes('localhost'));\n\n // Extract root domain for cookie clearing\n let domain: string | undefined;\n if (isProduction && hostname) {\n const parts = hostname.split('.');\n if (parts.length >= 2) {\n domain = '.' + parts.slice(-2).join('.');\n }\n }\n\n const cookieOptions = {\n path: '/',\n domain: domain // Use same domain as when setting\n };\n\n cookieUtils.remove(this.ACCESS_TOKEN, cookieOptions);\n cookieUtils.remove(this.REFRESH_TOKEN, cookieOptions);\n cookieUtils.remove(this.TENANT_ID, cookieOptions);\n \n // Also try to clear from current domain in case of legacy cookies\n if (domain) {\n const currentDomainOptions = { path: '/', domain: undefined };\n cookieUtils.remove(this.ACCESS_TOKEN, currentDomainOptions);\n cookieUtils.remove(this.REFRESH_TOKEN, currentDomainOptions);\n cookieUtils.remove(this.TENANT_ID, currentDomainOptions);\n }\n \n // Note: CSRF token is HTTP-only, cleared by server\n },\n\n /**\n * Check if user appears to be authenticated based on cookies\n * Returns true if either:\n * - Access token exists and is not expired\n * - Refresh token exists (can be used to get new access token)\n */\n hasValidTokens(): boolean {\n const accessToken = cookieUtils.get(this.ACCESS_TOKEN);\n const refreshToken = cookieUtils.get(this.REFRESH_TOKEN);\n\n // If we have a refresh token, user can be authenticated (via token refresh)\n if (refreshToken) {\n return true;\n }\n\n // No refresh token, check access token\n if (!accessToken) return false;\n\n // Check access token expiration\n try {\n const payload = JSON.parse(atob(accessToken.split('.')[1]));\n if (payload.exp) {\n const expiry = new Date(payload.exp * 1000);\n return expiry > new Date();\n }\n } catch {\n // If we can't decode, assume invalid\n return false;\n }\n\n return true;\n },\n\n /**\n * Get current tenant ID from cookie\n */\n getTenantId(): string | undefined {\n return cookieUtils.get(this.TENANT_ID);\n }\n};","import { ProdobitError, TokenInfo, type ProdobitClientConfig, type RequestConfig } from \"../types\";\nimport { tokenCookies, setCookiePrefix } from \"../utils/cookie-utils\";\n\nexport abstract class BaseClient {\n protected baseUrl: string;\n protected apiKey?: string;\n protected timeout: number;\n protected defaultHeaders: Record<string, string>;\n protected tokenInfo?: TokenInfo;\n protected autoRefresh: boolean;\n protected refreshPromise?: Promise<void>;\n\n constructor(config: ProdobitClientConfig) {\n this.baseUrl = config.baseUrl.replace(/\\/$/, \"\");\n this.apiKey = config.apiKey;\n this.timeout = config.timeout ?? 30000;\n this.autoRefresh = config.autoRefresh ?? true;\n this.defaultHeaders = {\n \"Content-Type\": \"application/json\",\n ...config.headers,\n };\n\n if (this.apiKey) {\n this.defaultHeaders[\"Authorization\"] = `Bearer ${this.apiKey}`;\n }\n\n // Set cookie prefix from config\n if (config.cookiePrefix) {\n setCookiePrefix(config.cookiePrefix);\n }\n\n // Load token from cookies if available\n this.tokenInfo = this.loadTokenFromCookies();\n }\n\n protected async request<T>(\n method: string,\n path: string,\n data?: unknown,\n config?: RequestConfig\n ): Promise<T> {\n // Check if token needs refresh before making the request\n // This handles both expiring tokens AND missing access tokens (when only refresh token exists)\n // BUT skip this check for refresh requests to avoid infinite recursion\n if (\n this.tokenInfo &&\n this.autoRefresh &&\n !config?.skipAuth &&\n path !== '/api/v1/auth/refresh' &&\n (this.isTokenExpiring() || !this.tokenInfo.accessToken)\n ) {\n await this.ensureTokenRefresh();\n }\n\n const url = `${this.baseUrl}${path}`;\n const headers = { ...this.defaultHeaders, ...config?.headers };\n const timeout = config?.timeout ?? this.timeout;\n\n // Add access token and CSRF token if available\n if (this.tokenInfo?.accessToken && !config?.skipAuth) {\n headers[\"Authorization\"] = `Bearer ${this.tokenInfo.accessToken}`;\n\n // CSRF token is now HTTP-only cookie, browser will send it automatically\n // We don't need to set it manually in headers\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n try {\n const response = await fetch(url, {\n method,\n headers,\n body: data ? JSON.stringify(data) : null,\n signal: controller.signal,\n credentials: 'include', // Include cookies for CSRF token\n });\n\n clearTimeout(timeoutId);\n\n // Handle 401 with auto-refresh\n if (\n response.status === 401 &&\n this.tokenInfo &&\n this.autoRefresh &&\n !config?.skipAuth\n ) {\n await this.ensureTokenRefresh();\n // Retry the request with new token\n return this.request(method, path, data, { ...config, skipAuth: false });\n }\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const message =\n errorData.error?.message || errorData.message || response.statusText;\n const code = errorData.error?.code || errorData.code;\n const details = errorData.error?.details || errorData.details;\n\n // Use factory methods for common HTTP status codes\n switch (response.status) {\n case 400:\n throw ProdobitError.badRequest(message, details);\n case 401:\n throw ProdobitError.unauthorized(message);\n case 403:\n throw ProdobitError.forbidden(message);\n case 404:\n throw ProdobitError.notFound(\"Resource\");\n case 409:\n throw ProdobitError.conflict(message, details);\n case 422:\n throw ProdobitError.validationError(message, details);\n case 500:\n throw ProdobitError.serverError(message, details);\n default:\n throw new ProdobitError(message, response.status, code, details);\n }\n }\n\n return await response.json();\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof ProdobitError) {\n throw error;\n }\n\n // Handle different error types\n if (error instanceof Error) {\n if (error.name === \"AbortError\") {\n throw ProdobitError.timeout(\"Request was aborted due to timeout\");\n }\n if (error.message.includes(\"fetch\")) {\n throw ProdobitError.networkError(`Network error: ${error.message}`);\n }\n throw ProdobitError.serverError(`Unexpected error: ${error.message}`);\n }\n\n throw ProdobitError.serverError(\"Unknown error occurred\");\n }\n }\n\n public async makeRequest<T = unknown>(\n method: string,\n path: string,\n data?: unknown,\n config?: RequestConfig\n ): Promise<T> {\n return this.request<T>(method, path, data, config);\n }\n\n private isTokenExpiring(): boolean {\n if (!this.tokenInfo) return false;\n\n // Check if token expires in the next 5 minutes\n const fiveMinutesFromNow = new Date(Date.now() + 5 * 60 * 1000);\n return this.tokenInfo.expiresAt <= fiveMinutesFromNow;\n }\n\n private async ensureTokenRefresh(): Promise<void> {\n if (this.refreshPromise) {\n return this.refreshPromise;\n }\n\n this.refreshPromise = this.performTokenRefresh();\n try {\n await this.refreshPromise;\n } finally {\n this.refreshPromise = undefined;\n }\n }\n\n private async performTokenRefresh(): Promise<void> {\n try {\n if