UNPKG

@prodobit/sdk

Version:

TypeScript SDK for Prodobit API

1,626 lines (1,612 loc) 154 kB
import { checkUserRequest, checkVerificationStatusRequest, cloneBomRequest, createBomComponentRequest, createBomRequest, createEcoRequest, createEmployeeRequest, createOrganizationRequest, createPersonRequest, createSalesOrderItemRequest, createSalesOrderRequest, logoutRequest, mrpRequirementsRequest, registerTenantRequest, rejectEcoRequest, requestOTPRequest, resendOTPRequest, resendVerificationEmailRequest, sendVerificationEmailRequest, updateBomRequest, updateEcoRequest, updateEmployeeRequest, updatePartyRequest, updateSalesOrderItemRequest, updateSalesOrderRequest, updateSalesOrderStatusRequest, verifyEmailRequest, verifyOTPRequest } from "@prodobit/types"; import { type } from "arktype"; export * from "@prodobit/types" //#region src/types.ts const prodobitClientConfig = type({ baseUrl: "string.url", "apiKey?": "string", "timeout?": "number >= 0", "headers?": "object", "autoRefresh?": "boolean", "persistToken?": "boolean", "tokenStorageKey?": "string" }); const requestConfig = type({ "headers?": "object", "timeout?": "number >= 0", "skipAuth?": "boolean" }); const sdkTokenInfo = type({ accessToken: "string", "refreshToken?": "string", expiresAt: "Date", "tenantId?": "string" }); const salesOrderQuery = type({ "status?": "string", "customerId?": "string", "orderDateFrom?": "string", "orderDateTo?": "string", "search?": "string", "page?": "number >= 1", "limit?": "number >= 1" }); const salesOrderFilters = salesOrderQuery; const itemFilters = type({ "search?": "string", "type?": "'product' | 'service' | 'raw_material' | 'component'", "status?": "'active' | 'inactive' | 'suspended' | 'deleted'", "categoryId?": "string", "page?": "number >= 1", "limit?": "number >= 1" }); const locationFilters = type({ "search?": "string", "type?": "string", "parentId?": "string", "status?": "'active' | 'inactive' | 'suspended' | 'deleted'", "page?": "number >= 1", "limit?": "number >= 1" }); const assetFilters = type({ "search?": "string", "type?": "string", "locationId?": "string", "status?": "'active' | 'inactive' | 'suspended' | 'deleted'", "page?": "number >= 1", "limit?": "number >= 1" }); const partyFilters = type({ "search?": "string", "type?": "'person' | 'organization'", "role?": "'customer' | 'supplier' | 'employee'", "status?": "'active' | 'inactive' | 'suspended' | 'deleted'", "page?": "number >= 1", "limit?": "number >= 1" }); const bomFilters = type({ "search?": "string", "itemId?": "string", "status?": "'draft' | 'approved' | 'suspended' | 'obsolete'", "page?": "number >= 1", "limit?": "number >= 1" }); const bomComponentFilters = type({ "bomId?": "string", "itemId?": "string", "page?": "number >= 1", "limit?": "number >= 1" }); const ecoFilters = type({ "search?": "string", "status?": "'draft' | 'in_review' | 'approved' | 'rejected' | 'implemented'", "bomId?": "string", "priority?": "'low' | 'medium' | 'high' | 'critical'", "page?": "number >= 1", "limit?": "number >= 1" }); const lotFilters = type({ "search?": "string", "itemId?": "string", "status?": "'active' | 'inactive' | 'suspended' | 'deleted'", "page?": "number >= 1", "limit?": "number >= 1" }); const stockFilters = type({ "itemId?": "string", "locationId?": "string", "lotId?": "string", "status?": "'available' | 'reserved' | 'quarantined' | 'damaged' | 'blocked'", "page?": "number >= 1", "limit?": "number >= 1" }); const stockTransactionFilters = type({ "itemId?": "string", "locationId?": "string", "lotId?": "string", "transactionType?": "string", "dateFrom?": "string", "dateTo?": "string", "page?": "number >= 1", "limit?": "number >= 1" }); const frameworkPartyFilters = partyFilters; const frameworkSearchParams = type({ searchTerm: "string", "partyType?": "'person' | 'organization'", "roleType?": "'customer' | 'supplier' | 'employee'" }); const frameworkEmployeeFilters = type({ "department?": "string", "role?": "string", "status?": "'active' | 'inactive' | 'on_leave'", "search?": "string", "page?": "number >= 1", "limit?": "number >= 1" }); const frameworkCustomerFilters = type({ "status?": "'active' | 'inactive' | 'suspended'", "customerType?": "'retail' | 'wholesale' | 'vip'", "region?": "string", "search?": "string", "page?": "number >= 1", "limit?": "number >= 1" }); const frameworkSupplierFilters = type({ "status?": "'active' | 'inactive' | 'suspended'", "category?": "string", "rating?": "number", "search?": "string", "page?": "number >= 1", "limit?": "number >= 1" }); const frameworkStockFilters = type({ "itemId?": "string", "locationId?": "string", "lotId?": "string", "status?": "'available' | 'reserved' | 'quarantined' | 'damaged' | 'blocked'", "lowStock?": "boolean", "search?": "string", "page?": "number >= 1", "limit?": "number >= 1" }); const frameworkLotFilters = lotFilters; const frameworkBomFilters = type({ "itemId?": "string", "status?": "'draft' | 'active' | 'obsolete'", "version?": "string", "effectiveDate?": "string.date", "search?": "string", "page?": "number >= 1", "limit?": "number >= 1" }); const frameworkBomComponentFilters = type({ "bomId?": "string", "componentId?": "string", "componentType?": "string", "required?": "boolean", "page?": "number >= 1", "limit?": "number >= 1" }); const contactInfo = type({ type: "'email' | 'phone'", value: "string", "isPrimary?": "boolean" }); const addressInfo = type({ type: "'billing' | 'shipping' | 'home'", line1: "string", "line2?": "string", city: "string", state: "string", postalCode: "string", country: "string" }); const createInvitationRequest = type({ email: "string.email", "role?": "string", "permissions?": "string[]" }); const updateMembershipRequest = type({ "role?": "string", "permissions?": "string[]", "status?": "'active' | 'inactive' | 'suspended'" }); const createStockRequest = type({ itemId: "string.uuid", locationId: "string.uuid", "lotId?": "string.uuid", quantity: "number >= 0", "unitCost?": "number >= 0", status: "'available' | 'reserved' | 'quarantined' | 'damaged' | 'blocked'" }); const updateStockRequest = type({ "quantity?": "number >= 0", "unitCost?": "number >= 0", "status?": "'available' | 'reserved' | 'quarantined' | 'damaged' | 'blocked'" }); const createLotRequest = type({ itemId: "string.uuid", lotNumber: "string >= 1", "expirationDate?": "string.date", "notes?": "string" }); const updateLotRequest = type({ "lotNumber?": "string >= 1", "expirationDate?": "string.date", "notes?": "string" }); const queryPrimitive = type("string | number | boolean | Date | null | undefined"); const queryValue = queryPrimitive.or(queryPrimitive.array()); const partyTypeEnum = type("'person' | 'organization'"); const roleTypeEnum = type("'customer' | 'supplier' | 'employee'"); const authState = type({ isAuthenticated: "boolean", isLoading: "boolean", isError: "boolean", "user?": "object | null", "token?": "object | null", "error?": "object | null", "tenantId?": "string | null" }); const cacheConfig = type({ defaultStaleTime: "number >= 0", defaultCacheTime: "number >= 0", "resourceStaleTime?": { "auth?": "number >= 0", "tenants?": "number >= 0", "parties?": "number >= 0", "items?": "number >= 0", "locations?": "number >= 0", "assets?": "number >= 0", "stocks?": "number >= 0", "lots?": "number >= 0", "boms?": "number >= 0", "salesOrders?": "number >= 0", "employees?": "number >= 0", "attributes?": "number >= 0", "ecos?": "number >= 0" }, refetchOnWindowFocus: "boolean", refetchOnReconnect: "boolean", refetchInterval: "number >= 0 | false", retry: "number >= 0 | boolean", retryDelay: "number >= 0" }); var ProdobitError = class ProdobitError extends Error { constructor(message, status, code, details) { super(message); this.status = status; this.code = code; this.details = details; this.name = "ProdobitError"; } static badRequest(message, details) { return new ProdobitError(message, 400, "BAD_REQUEST", details); } static unauthorized(message = "Unauthorized") { return new ProdobitError(message, 401, "UNAUTHORIZED"); } static forbidden(message = "Forbidden") { return new ProdobitError(message, 403, "FORBIDDEN"); } static notFound(resource, id) { const message = id ? `${resource} with ID '${id}' not found` : `${resource} not found`; return new ProdobitError(message, 404, "NOT_FOUND", { resource, id }); } static conflict(message, details) { return new ProdobitError(message, 409, "CONFLICT", details); } static validationError(message, details) { return new ProdobitError(message, 422, "VALIDATION_ERROR", details); } static serverError(message = "Internal Server Error", details) { return new ProdobitError(message, 500, "INTERNAL_ERROR", details); } static networkError(message = "Network Error") { return new ProdobitError(message, 0, "NETWORK_ERROR"); } static timeout(message = "Request Timeout") { return new ProdobitError(message, 408, "TIMEOUT"); } isNetworkError() { return this.code === "NETWORK_ERROR"; } isAuthError() { return this.status === 401 || this.status === 403; } isValidationError() { return this.code === "VALIDATION_ERROR"; } isNotFoundError() { return this.status === 404; } }; //#endregion //#region src/framework/auth-state.ts /** * Initial authentication state */ const initialAuthState = { isAuthenticated: false, isLoading: false, isError: false, user: null, token: null, error: null, tenantId: null }; /** * Authentication State Reducer */ function authReducer(state, action) { switch (action.type) { case "AUTH_START": return { ...state, isLoading: true, isError: false, error: null }; case "AUTH_SUCCESS": return { ...state, isLoading: false, isError: false, isAuthenticated: true, user: action.payload.user, token: action.payload.token, tenantId: action.payload.token.tenantId || null, error: null }; case "AUTH_ERROR": return { ...state, isLoading: false, isError: true, isAuthenticated: false, user: null, token: null, tenantId: null, error: typeof action.payload.error === "string" ? new Error(action.payload.error) : action.payload.error }; case "AUTH_LOGOUT": return { ...initialAuthState }; case "TOKEN_REFRESH": return { ...state, token: action.payload.token, tenantId: action.payload.token.tenantId || state.tenantId, error: state.error?.isAuthError?.() ? null : state.error }; case "SET_TENANT": return { ...state, tenantId: action.payload.tenantId }; case "CLEAR_ERROR": return { ...state, isError: false, error: null }; default: return state; } } /** * Authentication State Manager * Provides a framework-agnostic way to manage authentication state */ var AuthStateManager = class { state = initialAuthState; listeners = /* @__PURE__ */ new Set(); client; refreshTimer = null; isInitialized = false; constructor(client) { this.client = client; if (this.client.getTokenInfo()) this.state = { ...initialAuthState, isLoading: true }; this.setupAutoRefresh(); } /** * Subscribe to auth state changes */ subscribe(listener) { this.listeners.add(listener); listener(this.state); return () => { this.listeners.delete(listener); }; } /** * Get current auth state */ getState() { return this.state; } /** * Update state and notify listeners */ setState(action) { const newState = authReducer(this.state, action); const hasChanged = newState !== this.state; this.state = newState; if (hasChanged) this.listeners.forEach((listener) => listener(this.state)); } /** * Initialize authentication from stored token */ async initialize() { if (this.isInitialized) return; this.isInitialized = true; try { this.setState({ type: "AUTH_START" }); const currentToken = this.client.getTokenInfo(); const isTokenValid = this.client.isTokenValid(); if (!currentToken || !isTokenValid) try { await this.client.refreshToken(); } catch (error) { this.setState({ type: "AUTH_LOGOUT" }); return; } const userResponse = await this.client.getCurrentUser(); if (userResponse.success && userResponse.data) this.setState({ type: "AUTH_SUCCESS", payload: { user: userResponse.data.user, token: this.client.getTokenInfo() } }); else throw ProdobitError.unauthorized("Failed to get user info"); } catch (error) { this.setState({ type: "AUTH_ERROR", payload: { error: error instanceof ProdobitError ? error : ProdobitError.serverError("Authentication initialization failed") } }); } } /** * Login with OTP */ async loginWithOTP(email, tenantId) { try { this.setState({ type: "AUTH_START" }); const response = await this.client.requestOTP({ email, tenantId }); if (response.success) return { success: true, expiresAt: response.expiresAt }; else { const error = ProdobitError.badRequest("Failed to send OTP"); this.setState({ type: "AUTH_ERROR", payload: { error } }); return { success: false, error: error.message }; } } catch (error) { const authError = error instanceof ProdobitError ? error : ProdobitError.serverError("OTP request failed"); this.setState({ type: "AUTH_ERROR", payload: { error: authError } }); return { success: false, error: authError.message }; } } /** * Verify OTP and complete login */ async verifyOTP(email, code, tenantId) { try { this.setState({ type: "AUTH_START" }); const response = await this.client.verifyOTP({ email, code, tenantId }); if (response.success && response.data) { const token = this.client.getTokenInfo(); if (token) { this.setState({ type: "AUTH_SUCCESS", payload: { user: response.data.user, token } }); return { success: true, user: response.data.user }; } } const error = ProdobitError.unauthorized("OTP verification failed"); this.setState({ type: "AUTH_ERROR", payload: { error } }); return { success: false, error: error.message }; } catch (error) { const authError = error instanceof ProdobitError ? error : ProdobitError.unauthorized("OTP verification failed"); this.setState({ type: "AUTH_ERROR", payload: { error: authError } }); return { success: false, error: authError.message }; } } /** * Refresh authentication token */ async refreshToken() { try { const response = await this.client.refreshToken(); if (response.success && response.data) { const token = this.client.getTokenInfo(); if (token) { this.setState({ type: "TOKEN_REFRESH", payload: { token } }); this.setupAutoRefresh(); } } else throw ProdobitError.unauthorized("Token refresh failed"); } catch (error) { const authError = error instanceof ProdobitError ? error : ProdobitError.unauthorized("Token refresh failed"); this.setState({ type: "AUTH_ERROR", payload: { error: authError } }); throw authError; } } /** * Logout user */ async logout(allDevices = false) { try { await this.client.logout({ allDevices }); } catch (error) { console.warn("Logout API call failed:", error); } finally { this.clearRefreshTimer(); this.setState({ type: "AUTH_LOGOUT" }); } } /** * Set current tenant */ setTenant(tenantId) { this.setState({ type: "SET_TENANT", payload: { tenantId } }); } /** * Clear authentication error */ clearError() { this.setState({ type: "CLEAR_ERROR" }); } /** * Setup automatic token refresh */ setupAutoRefresh() { this.clearRefreshTimer(); const token = this.client.getTokenInfo(); if (!token) return; const refreshTime = token.expiresAt.getTime() - Date.now() - 300 * 1e3; if (refreshTime > 0) this.refreshTimer = setTimeout(async () => { try { await this.refreshToken(); } catch (error) { console.warn("Automatic token refresh failed:", error); this.setState({ type: "AUTH_LOGOUT" }); } }, refreshTime); } /** * Clear refresh timer */ clearRefreshTimer() { if (this.refreshTimer) { clearTimeout(this.refreshTimer); this.refreshTimer = null; } } /** * Cleanup resources */ destroy() { this.clearRefreshTimer(); this.listeners.clear(); } }; /** * Authentication helpers for different frameworks */ const authHelpers = { createStateSelector: (selector) => (state) => selector(state), selectors: { isAuthenticated: (state) => state.isAuthenticated, isLoading: (state) => state.isLoading, isError: (state) => state.isError, user: (state) => state.user, token: (state) => state.token, error: (state) => state.error, tenantId: (state) => state.tenantId, isReady: (state) => !state.isLoading && !state.isError, hasUser: (state) => state.isAuthenticated && !!state.user, hasTenant: (state) => !!state.tenantId, hasAuthError: (state) => !!state.error?.isAuthError?.(), hasNetworkError: (state) => !!state.error?.isNetworkError?.(), hasValidationError: (state) => !!state.error?.isValidationError?.() }, actions: { startAuth: () => ({ type: "AUTH_START" }), authSuccess: (user, token) => ({ type: "AUTH_SUCCESS", payload: { user, token } }), authError: (error) => ({ type: "AUTH_ERROR", payload: { error } }), logout: () => ({ type: "AUTH_LOGOUT" }), refreshToken: (token) => ({ type: "TOKEN_REFRESH", payload: { token } }), setTenant: (tenantId) => ({ type: "SET_TENANT", payload: { tenantId } }), clearError: () => ({ type: "CLEAR_ERROR" }) } }; /** * Token management utilities */ const tokenUtils = { isExpiringSoon: (token, thresholdMinutes = 5) => { const threshold = thresholdMinutes * 60 * 1e3; return token.expiresAt.getTime() - Date.now() < threshold; }, getTimeUntilExpiration: (token) => { return Math.max(0, token.expiresAt.getTime() - Date.now()); }, formatExpiration: (token) => { const timeLeft = tokenUtils.getTimeUntilExpiration(token); const minutes = Math.floor(timeLeft / (60 * 1e3)); const hours = Math.floor(minutes / 60); if (hours > 0) return `${hours}h ${minutes % 60}m`; return `${minutes}m`; }, decodeTokenPayload: (token) => { try { const payload = token.split(".")[1]; return JSON.parse(atob(payload)); } catch { return null; } } }; //#endregion //#region src/utils/validation.ts function validateRequest(schema, data, errorMessage = "Invalid request data") { const result = schema(data); if (result instanceof type.errors) throw ProdobitError.validationError(errorMessage, result.summary); return result; } //#endregion //#region src/modules/base-client.ts var BaseClient = class { baseUrl; apiKey; timeout; defaultHeaders; tokenInfo; autoRefresh; refreshPromise; constructor(config) { this.baseUrl = config.baseUrl.replace(/\/$/, ""); this.apiKey = config.apiKey; this.timeout = config.timeout ?? 3e4; this.autoRefresh = config.autoRefresh ?? true; this.defaultHeaders = { "Content-Type": "application/json", ...config.headers }; if (this.apiKey) this.defaultHeaders["Authorization"] = `Bearer ${this.apiKey}`; this.tokenInfo = this.loadTokenFromSession(); } async request(method, path, data, config) { if (this.tokenInfo && this.autoRefresh && this.isTokenExpiring()) await this.ensureTokenRefresh(); const url = `${this.baseUrl}${path}`; const headers = { ...this.defaultHeaders, ...config?.headers }; const timeout = config?.timeout ?? this.timeout; if (this.tokenInfo && !config?.skipAuth) { headers["Authorization"] = `Bearer ${this.tokenInfo.accessToken}`; if (this.tokenInfo.csrfToken) headers["X-CSRF-Token"] = this.tokenInfo.csrfToken; } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { method, headers, body: data ? JSON.stringify(data) : null, signal: controller.signal, credentials: "same-origin" }); clearTimeout(timeoutId); if (response.status === 401 && this.tokenInfo && this.autoRefresh && !config?.skipAuth) { await this.ensureTokenRefresh(); return this.request(method, path, data, { ...config, skipAuth: false }); } if (!response.ok) { const errorData = await response.json().catch(() => ({})); const message = errorData.error?.message || errorData.message || response.statusText; const code = errorData.error?.code || errorData.code; const details = errorData.error?.details || errorData.details; switch (response.status) { case 400: throw ProdobitError.badRequest(message, details); case 401: throw ProdobitError.unauthorized(message); case 403: throw ProdobitError.forbidden(message); case 404: throw ProdobitError.notFound("Resource"); case 409: throw ProdobitError.conflict(message, details); case 422: throw ProdobitError.validationError(message, details); case 500: throw ProdobitError.serverError(message, details); default: throw new ProdobitError(message, response.status, code, details); } } return await response.json(); } catch (error) { clearTimeout(timeoutId); if (error instanceof ProdobitError) throw error; if (error instanceof Error) { if (error.name === "AbortError") throw ProdobitError.timeout("Request was aborted due to timeout"); if (error.message.includes("fetch")) throw ProdobitError.networkError(`Network error: ${error.message}`); throw ProdobitError.serverError(`Unexpected error: ${error.message}`); } throw ProdobitError.serverError("Unknown error occurred"); } } async makeRequest(method, path, data, config) { return this.request(method, path, data, config); } isTokenExpiring() { if (!this.tokenInfo) return false; const fiveMinutesFromNow = new Date(Date.now() + 300 * 1e3); const isExpiring = this.tokenInfo.expiresAt <= fiveMinutesFromNow; if (isExpiring) console.log("Token expiring soon:", { expiresAt: this.tokenInfo.expiresAt, now: /* @__PURE__ */ new Date(), fiveMinutesFromNow, isExpiring }); return isExpiring; } async ensureTokenRefresh() { if (this.refreshPromise) return this.refreshPromise; this.refreshPromise = this.performTokenRefresh(); try { await this.refreshPromise; } finally { this.refreshPromise = void 0; } } async performTokenRefresh() { try { const response = await this.request("POST", "/api/v1/auth/refresh", { refreshToken: this.tokenInfo?.refreshToken }, { skipAuth: true }); if (response.success && response.data) this.setTokenInfo({ accessToken: response.data.session.accessToken, refreshToken: response.data.session.refreshToken, expiresAt: new Date(response.data.session.expiresAt), csrfToken: response.data.session.csrfToken, tenantId: this.tokenInfo?.tenantId }); } catch (error) { this.clearTokenInfo(); throw error; } } loadTokenFromSession() { if (typeof window === "undefined") return void 0; try { const stored = sessionStorage.getItem("prodobit_token"); if (!stored) return void 0; const parsed = JSON.parse(stored); if (parsed.expiresAt && new Date(parsed.expiresAt) <= /* @__PURE__ */ new Date()) { this.clearTokenInfo(); return; } return { ...parsed, expiresAt: parsed.expiresAt ? new Date(parsed.expiresAt) : void 0 }; } catch { this.clearTokenInfo(); return; } } setTokenInfo(tokenInfo) { this.tokenInfo = tokenInfo; if (typeof window !== "undefined") try { const tokenToStore = { accessToken: tokenInfo.accessToken, refreshToken: tokenInfo.refreshToken, expiresAt: tokenInfo.expiresAt, csrfToken: tokenInfo.csrfToken, tenantId: tokenInfo.tenantId }; sessionStorage.setItem("prodobit_token", JSON.stringify(tokenToStore)); } catch {} } getTokenInfo() { return this.tokenInfo; } clearTokenInfo() { this.tokenInfo = void 0; if (typeof window !== "undefined") try { sessionStorage.removeItem("prodobit_token"); } catch {} } getCurrentTenantId() { if (!this.tokenInfo?.accessToken) return; try { return JSON.parse(atob(this.tokenInfo.accessToken.split(".")[1])).tenantId; } catch (error) { return; } } isAuthenticated() { return !!this.tokenInfo?.accessToken && this.isTokenValid(); } isTokenValid() { if (!this.tokenInfo) return false; return this.tokenInfo.expiresAt > /* @__PURE__ */ new Date(); } getAccessToken() { return this.tokenInfo?.accessToken; } setApiKey(apiKey) { this.apiKey = apiKey; this.defaultHeaders["Authorization"] = `Bearer ${apiKey}`; } removeApiKey() { this.apiKey = void 0; delete this.defaultHeaders["Authorization"]; } }; //#endregion //#region src/modules/auth-client.ts var AuthClient = class extends BaseClient { stateManager; getStateManager() { if (!this.stateManager) { const proxyClient = { refreshToken: this.refreshToken.bind(this), getCurrentUser: this.getCurrentUser.bind(this), getTokenInfo: this.getTokenInfo.bind(this), logout: this.logout.bind(this) }; this.stateManager = new AuthStateManager(proxyClient); } return this.stateManager; } getState() { return this.getStateManager().getState(); } subscribe(listener) { return this.getStateManager().subscribe(listener); } async initialize() { return this.getStateManager().initialize(); } async checkUser(data, config) { const validatedData = validateRequest(checkUserRequest, data); return await this.request("POST", "/api/v1/auth/check-user", validatedData, { ...config, skipAuth: true }); } async registerTenant(data, config) { const validatedData = validateRequest(registerTenantRequest, data); return await this.request("POST", "/api/v1/auth/register-tenant", validatedData, { ...config, skipAuth: true }); } async requestOTP(data, config) { const validatedData = validateRequest(requestOTPRequest, data); return await this.request("POST", "/api/v1/auth/request-otp", validatedData, { ...config, skipAuth: true }); } async resendOTP(data, config) { const validatedData = validateRequest(resendOTPRequest, data); return await this.request("POST", "/api/v1/auth/resend-otp", validatedData, { ...config, skipAuth: true }); } async verifyOTP(data, config) { const validatedData = validateRequest(verifyOTPRequest, data); const response = await this.request("POST", "/api/v1/auth/verify-otp", validatedData, { ...config, skipAuth: true }); if (response.success && response.data) { let tenantId; try { tenantId = JSON.parse(atob(response.data.session.accessToken.split(".")[1])).tenantId; } catch { if (response.data.tenantMemberships && response.data.tenantMemberships.length > 0) tenantId = response.data.tenantMemberships[0]?.tenantId; } console.log("Login success, storing tokens:", { hasAccessToken: !!response.data.session.accessToken, hasRefreshToken: !!response.data.refreshToken, hasCsrfToken: !!response.data.session.csrfToken }); this.setTokenInfo({ accessToken: response.data.session.accessToken, refreshToken: response.data.refreshToken, expiresAt: new Date(response.data.session.expiresAt), csrfToken: response.data.session.csrfToken, tenantId }); } return response; } async refreshToken(data, config) { if (!this.tokenInfo?.refreshToken) throw new ProdobitError("No refresh token available", 401, "REFRESH_TOKEN_MISSING"); console.log("Refreshing with token:", this.tokenInfo.refreshToken?.substring(0, 20) + "..."); const response = await this.request("POST", "/api/v1/auth/refresh", { refreshToken: this.tokenInfo.refreshToken }, { ...config, skipAuth: true }); if (response.success && response.data) this.setTokenInfo({ accessToken: response.data.session.accessToken, refreshToken: response.data.refreshToken, expiresAt: new Date(response.data.session.expiresAt), csrfToken: response.data.session.csrfToken, tenantId: this.tokenInfo?.tenantId }); return response; } async logout(data, config) { const logoutData = { allDevices: false, ...data }; const validatedData = validateRequest(logoutRequest, logoutData); const response = await this.request("POST", "/api/v1/auth/logout", validatedData, config); this.clearTokenInfo(); return response; } async getCurrentUser(config) { return this.request("GET", "/api/v1/auth/me", void 0, config); } async getMe(config) { return this.getCurrentUser(config); } async loginWithOTP(email, tenantId) { const response = await this.requestOTP({ email, ...tenantId && { tenantId } }); return { success: response.success, message: response.message, expiresAt: response.expiresAt, requiresTenantSelection: response.requiresTenantSelection, isNewUser: response.isNewUser, defaultTenantId: response.defaultTenantId, defaultTenantName: response.defaultTenantName, selectedTenantId: response.selectedTenantId, selectedTenantName: response.selectedTenantName, tenants: response.tenants }; } async completeLogin(email, code, tenantId) { try { const response = await this.verifyOTP({ email, code, ...tenantId && { tenantId } }); if (response.success && response.data) return { success: true, user: response.data.user, authMethod: response.data.authMethod, isNewUser: response.data.isNewUser, session: response.data.session, tenantMemberships: response.data.tenantMemberships }; return { success: false, error: "Verification failed" }; } catch (error) { return { success: false, error: error instanceof ProdobitError ? error.message : "Unknown error" }; } } async signOut(allDevices = false) { try { await this.logout({ allDevices }); return true; } catch (error) { this.clearTokenInfo(); return false; } } /** * Force refresh auth state - useful after login to ensure everything is synced */ async refreshAuthState() { console.log("refreshAuthState called, isAuthenticated:", this.isAuthenticated()); if (this.isAuthenticated()) try { console.log("Calling refreshToken from refreshAuthState"); await this.refreshToken(); } catch (error) { console.log("refreshAuthState failed:", error); this.clearTokenInfo(); throw error; } } async sendVerificationEmail(data, config) { const validatedData = validateRequest(sendVerificationEmailRequest, data); return await this.request("POST", "/api/v1/auth/send-verification-email", validatedData, { ...config, skipAuth: true }); } async verifyEmail(data, config) { const validatedData = validateRequest(verifyEmailRequest, data); return await this.request("GET", `/api/v1/auth/verify-email/${encodeURIComponent(validatedData.token)}`, void 0, { ...config, skipAuth: true }); } async resendVerificationEmail(data, config) { const validatedData = validateRequest(resendVerificationEmailRequest, data); return await this.request("POST", "/api/v1/auth/resend-verification-email", validatedData, { ...config, skipAuth: true }); } async checkVerificationStatus(data, config) { const validatedData = validateRequest(checkVerificationStatusRequest, data); return await this.request("POST", "/api/v1/auth/check-verification-status", validatedData, { ...config, skipAuth: true }); } }; //#endregion //#region src/utils/query-builder.ts /** General purpose pure query builder */ function buildQuery(filters) { if (!filters) return ""; const params = new URLSearchParams(); for (const [key, raw] of Object.entries(filters)) { if (raw == null) continue; const append = (v) => { if (v == null) return; const val = v instanceof Date ? v.toISOString() : typeof v === "boolean" ? String(v) : String(v); params.append(key, val); }; if (Array.isArray(raw)) for (const v of raw) append(v); else append(raw); } return params.toString(); } //#endregion //#region src/modules/tenant-client.ts var TenantClient = class extends BaseClient { async getTenants(query, config) { const queryString = buildQuery(query); const path = `/api/v1/tenants${queryString ? `?${queryString}` : ""}`; return this.request("GET", path, void 0, config); } async getTenant(id, config) { return this.request("GET", `/api/v1/tenants/${id}`, void 0, config); } async createTenant(data, config) { return this.request("POST", "/api/v1/tenants", data, config); } async updateTenant(id, data, config) { return this.request("PUT", `/api/v1/tenants/${id}`, data, config); } async deleteTenant(id, config) { return this.request("DELETE", `/api/v1/tenants/${id}`, void 0, config); } async getTenantMembers(tenantId, config) { return this.request("GET", `/api/v1/tenants/${tenantId}/members`, void 0, config); } async getTenantRoles(tenantId, config) { return this.request("GET", `/api/v1/tenants/${tenantId}/roles`, void 0, config); } async getTenantInvitations(tenantId, config) { return this.request("GET", `/api/v1/tenants/${tenantId}/invitations`, void 0, config); } async createInvitation(tenantId, data, config) { return this.request("POST", `/api/v1/tenants/${tenantId}/invitations`, data, config); } async getInvitationByToken(token, config) { return this.request("GET", `/api/v1/invitations/${token}`, void 0, config); } async acceptInvitation(token, config) { return this.request("POST", `/api/v1/invitations/${token}/accept`, void 0, config); } async updateMembership(tenantId, membershipId, data, config) { return this.request("PATCH", `/api/v1/tenants/${tenantId}/members/${membershipId}`, data, config); } async removeMember(tenantId, membershipId, config) { return this.request("DELETE", `/api/v1/tenants/${tenantId}/members/${membershipId}`, void 0, config); } }; //#endregion //#region src/modules/party-client.ts var PartyClient = class extends BaseClient { async createPerson(data, config) { const validatedData = validateRequest(createPersonRequest, data); return this.request("POST", "/api/v1/parties/persons", validatedData, config); } async createOrganization(data, config) { const validatedData = validateRequest(createOrganizationRequest, data); return this.request("POST", "/api/v1/parties/organizations", validatedData, config); } async getParties(query, config) { const queryString = buildQuery(query); const path = `/api/v1/parties${queryString ? `?${queryString}` : ""}`; return this.request("GET", path, void 0, config); } async getParty(id, config) { return this.request("GET", `/api/v1/parties/${id}`, void 0, config); } async updateParty(id, data, config) { const validatedData = validateRequest(updatePartyRequest, data); return this.request("PUT", `/api/v1/parties/${id}`, validatedData, config); } async deleteParty(id, config) { return this.request("DELETE", `/api/v1/parties/${id}`, void 0, config); } async getCustomers(config) { return this.request("GET", "/api/v1/customers", void 0, config); } async getSuppliers(config) { return this.request("GET", "/api/v1/suppliers", void 0, config); } async getEmployeeParties(config) { return this.request("GET", "/api/v1/parties/employees", void 0, config); } async createParty(data, config) { return this.request("POST", "/api/v1/parties", data, config); } /** * Create a person customer with minimal data */ async createPersonCustomer(firstName, lastName, email, phone, config) { const data = { firstName, lastName, roles: ["customer"], ...email && { contacts: [{ contactType: "email", contactValue: email, isPrimary: true }] }, ...phone && { contacts: [{ contactType: "phone", contactValue: phone, isPrimary: !email }] } }; if (email && phone) data.contacts = [{ contactType: "email", contactValue: email, isPrimary: true }, { contactType: "phone", contactValue: phone, isPrimary: false }]; return this.createPerson(data, config); } /** * Create a person supplier with minimal data */ async createPersonSupplier(firstName, lastName, email, phone, config) { const data = { firstName, lastName, roles: ["supplier"], ...email && { contacts: [{ contactType: "email", contactValue: email, isPrimary: true }] }, ...phone && { contacts: [{ contactType: "phone", contactValue: phone, isPrimary: !email }] } }; if (email && phone) data.contacts = [{ contactType: "email", contactValue: email, isPrimary: true }, { contactType: "phone", contactValue: phone, isPrimary: false }]; return this.createPerson(data, config); } /** * Create an organization customer with minimal data */ async createOrganizationCustomer(name, email, phone, address, config) { const data = { name, roles: ["customer"], ...email && { contacts: [{ contactType: "email", contactValue: email, isPrimary: true }] }, ...phone && { contacts: [{ contactType: "phone", contactValue: phone, isPrimary: !email }] }, ...address && { addresses: [{ addressType: "billing", line1: address, country: "US", isPrimary: true }] } }; if (email && phone) data.contacts = [{ contactType: "email", contactValue: email, isPrimary: true }, { contactType: "phone", contactValue: phone, isPrimary: false }]; return this.createOrganization(data, config); } /** * Create an organization supplier with minimal data */ async createOrganizationSupplier(name, email, phone, address, config) { const data = { name, roles: ["supplier"], ...email && { contacts: [{ contactType: "email", contactValue: email, isPrimary: true }] }, ...phone && { contacts: [{ contactType: "phone", contactValue: phone, isPrimary: !email }] }, ...address && { addresses: [{ addressType: "billing", line1: address, country: "US", isPrimary: true }] } }; if (email && phone) data.contacts = [{ contactType: "email", contactValue: email, isPrimary: true }, { contactType: "phone", contactValue: phone, isPrimary: false }]; return this.createOrganization(data, config); } /** * Create a person with multiple roles (e.g., customer + supplier) */ async createPersonWithRoles(firstName, lastName, roles, contacts, addresses, config) { const data = { firstName, lastName, roles, ...contacts && { contacts: contacts.map((c) => ({ contactType: c.type, contactValue: c.value, isPrimary: c.isPrimary || false })) }, ...addresses && { addresses: addresses.map((a) => ({ addressType: a.type, line1: a.line1, city: a.city, country: "US", isPrimary: false })) } }; return this.createPerson(data, config); } /** * Search parties by name/email/phone */ async searchParties(searchTerm, partyType, roleType, config) { const params = new URLSearchParams(); params.append("search", searchTerm); if (partyType) params.append("partyType", partyType); if (roleType) params.append("roleType", roleType); return this.request("GET", `/api/v1/parties/search?${params.toString()}`, void 0, config); } }; //#endregion //#region src/modules/location-asset-client.ts var LocationAssetClient = class extends BaseClient { async getLocations(filters, config) { const queryString = buildQuery(filters); const path = `/api/v1/locations${queryString ? `?${queryString}` : ""}`; return this.request("GET", path, void 0, config); } async getLocation(locationId, config) { return this.request("GET", `/api/v1/locations/${locationId}`, void 0, config); } async getLocationById(locationId, config) { return this.getLocation(locationId, config); } async getChildLocations(parentLocationId, config) { return this.request("GET", `/api/v1/locations/${parentLocationId}/children`, void 0, config); } async getLocationHierarchy(locationId, config) { return this.request("GET", `/api/v1/locations/${locationId}/hierarchy`, void 0, config); } async createLocation(data, config) { return this.request("POST", "/api/v1/locations", data, config); } async updateLocation(locationId, data, config) { return this.request("PUT", `/api/v1/locations/${locationId}`, data, config); } async deleteLocation(locationId, config) { return this.request("DELETE", `/api/v1/locations/${locationId}`, void 0, config); } async getLocationStats(config) { return this.request("GET", "/api/v1/locations/stats", void 0, config); } async getLocationTypes(category, config) { const queryString = category ? `?category=${category}` : ""; return this.request("GET", `/api/v1/locations/types${queryString}`, void 0, config); } async createLocationType(data, config) { return this.request("POST", "/api/v1/locations/types", data, config); } async getAssets(filters, config) { const queryString = buildQuery(filters); const path = `/api/v1/assets${queryString ? `?${queryString}` : ""}`; return this.request("GET", path, void 0, config); } async searchAssets(searchTerm, filters, config) { const params = new URLSearchParams(); params.append("q", searchTerm); if (filters) Object.entries(filters).forEach(([key, value]) => { if (value !== void 0) params.append(key, String(value)); }); return this.request("GET", `/api/v1/assets/search?${params.toString()}`, void 0, config); } async getAsset(assetId, config) { return this.request("GET", `/api/v1/assets/${assetId}`, void 0, config); } async getAssetById(assetId, config) { return this.getAsset(assetId, config); } async getChildAssets(parentAssetId, config) { return this.request("GET", `/api/v1/assets/${parentAssetId}/children`, void 0, config); } async getAssetHierarchy(assetId, config) { return this.request("GET", `/api/v1/assets/${assetId}/hierarchy`, void 0, config); } async getAssetsByLocation(locationId, config) { return this.request("GET", `/api/v1/assets/by-location/${locationId}`, void 0, config); } async createAsset(data, config) { return this.request("POST", "/api/v1/assets", data, config); } async updateAsset(assetId, data, config) { return this.request("PUT", `/api/v1/assets/${assetId}`, data, config); } async moveAsset(assetId, locationId, config) { return this.request("PUT", `/api/v1/assets/${assetId}/move`, { locationId }, config); } async deleteAsset(assetId, config) { return this.request("DELETE", `/api/v1/assets/${assetId}`, void 0, config); } async getAssetStats(config) { return this.request("GET", "/api/v1/assets/stats", void 0, config); } async getAssetTypes(category, config) { const queryString = category ? `?category=${category}` : ""; return this.request("GET", `/api/v1/assets/types${queryString}`, void 0, config); } async createAssetType(data, config) { return this.request("POST", "/api/v1/assets/types", data, config); } async createLocationQuick(name, locationType = "area", parentLocationId, config) { const data = { name, locationType, parentLocationId }; return this.createLocation(data, config); } async createAssetQuick(name, locationId, assetType = "equipment", config) { const data = { name, locationId, assetType }; return this.createAsset(data, config); } }; //#endregion //#region src/modules/manufacturing-client.ts var ManufacturingClient = class extends BaseClient { async getBoms(filters, config) { const query = buildQuery(filters); return this.request("GET", `/api/v1/manufacturing/boms${query}`, void 0, config); } async getBomById(id, config) { return this.request("GET", `/api/v1/manufacturing/boms/${id}`, void 0, config); } async createBom(data, config) { const validatedData = validateRequest(createBomRequest, data); return this.request("POST", "/api/v1/manufacturing/boms", validatedData, config); } async updateBom(id, data, config) { const validatedData = validateRequest(updateBomRequest, data); return this.request("PUT", `/api/v1/manufacturing/boms/${id}`, validatedData, config); } async deleteBom(id, config) { return this.request("DELETE", `/api/v1/manufacturing/boms/${id}`, void 0, config); } async getBomExplosion(id, explodePhantoms = true, config) { const query = explodePhantoms ? "?explodePhantoms=true" : "?explodePhantoms=false"; return this.request("GET", `/api/v1/manufacturing/boms/${id}/explosion${query}`, void 0, config); } async cloneBom(id, data, config) { const validatedData = validateRequest(cloneBomRequest, data); return this.request("POST", `/api/v1/manufacturing/boms/${id}/clone`, validatedData, config); } async addBomComponent(data, config) { const validatedData = validateRequest(createBomComponentRequest, data); return this.request("POST", "/api/v1/manufacturing/bom-components", validatedData, config); } async getBomComponents(filters, config) { const query = buildQuery(filters); return this.request("GET", `/api/v1/manufacturing/bom-components${query}`, void 0, config); } async updateBomComponent(id, data, config) { return this.request("PUT", `/api/v1/manufacturing/bom-components/${id}`, data, config); } async deleteBomComponent(id, config) { return this.request("DELETE", `/api/v1/manufacturing/bom-components/${id}`, void 0, config); } async getEcos(filters, config) { const query = buildQuery(filters); return this.request("GET", `/api/v1/manufacturing/ecos${query}`, void 0, config); } async getEcoById(id, config) { return this.request("GET", `/api/v1/manufacturing/ecos/${id}`, void 0, config); } async createEco(data, config) { const validatedData = validateRequest(createEcoRequest, data); return this.request("POST", "/api/v1/manufacturing/ecos", validatedData, config); } async updateEco(id, data, config) { const validatedData = validateRequest(updateEcoRequest, data); return this.request("PUT", `/api/v1/manufacturing/ecos/${id}`, validatedData, config); } async approveEco(id, config) { return this.request("POST", `/api/v1/manufacturing/ecos/${id}/approve`, void 0, config); } async rejectEco(id, data, config) { const validatedData = validateRequest(rejectEcoRequest, data); return this.request("POST", `/api/v1/manufacturing/ecos/${id}/reject`, validatedData, config); } async runMrpRequirements(data, config) { const validatedData = validateRequest(mrpRequirementsRequest, data); return this.request("POST", "/api/v1/manufacturing/mrp/requirements", validatedData, config); } async getBomLeadTime(bomId, config) { return this.request("GET", `/api/v1/manufacturing/boms/${bomId}/lead-time`, void 0, config); } async getBomStats(filters, config) { const query = buildQuery(filters); return this.request("GET", `/api/v1/manufacturing/boms/stats${query}`, void 0, config); } async createBomQuick(data, config) { return this.request("POST", "/api/v1/manufacturing/boms/quick", data, config); } }; //#endregion //#region src/modules/sales-client.ts var SalesClient = class extends BaseClient { async getSalesOrders(filters, config) { const query = buildQuery(filters); return this.request("GET", `/api/v1/sales${query}`, void 0, config); } async getSalesOrderById(id, config) { return this.request("GET", `/api/v1/sales/${id}`, void 0, config); } async createSalesOrder(data, config) { const validatedData = validateRequest(createSalesOrderRequest, data); return this.request("POST", "/api/v1/sales", validatedData, config); } async updateSalesOrder(id, data, config) { const validatedData = validateRequest(updateSalesOrderRequest, data); return this.request("PUT", `/api/v1/sales/${id}`, validatedData, config); } async updateSalesOrderStatus(id, data, config) { const validatedData = validateRequest(updateSalesOrderStatusRequest, data); return this.request("PUT", `/api/v1/sales/${id}/status`, validatedData, config); } async deleteSalesOrder(id, config) { return this.request("DELETE", `/api/v1/sales/${id}`, void 0, config); } async addSalesOrderItem(salesOrderId, data, config) { const validatedData = validateRequest(createSalesOrderItemRequest, data); return this.request("POST", `/api/v1/sales/${salesOrderId}/items`, validatedData, config); } async updateSalesOrderItem(salesOrderId, itemId, data, config) { const validatedData = validateRequest(updateSalesOrderItemRequest, data); return this.request("PUT", `/api/v1/sales/${salesOrderId}/items/${itemId}`, validatedData, config); } async removeSalesOrderItem(salesOrderId, itemId, config) { return this.request("DELETE", `/api/v1/sales/${salesOrderId}/items/${itemId}`, void 0, config); } async getSalesOrderHistory(id, config) { return this.request("GET", `/api/v1/sales/${id}/history`, void 0, config); } }; //#endregion //#region src/modules/employee-client.ts var EmployeeClient = class extends BaseClient { async getEmployees(config) { return this.request("GET", "/api/v1/employees", void 0, config); } async getEmployeeById(id, config) { return this.request("GET", `/api/v1/employees/${id}`, void 0, config); } async createEmployee(data, config) { const validatedData = validateRequest(createEmployeeRequest, data); return this.request("POST", "/api/v1/employees", validatedData, config); } async updateEmployee(id, data, config) { const validatedData = validateRequest(updateEmployeeRequest, data); return this.request("PUT", `/api/v1/employees/${id}`, validatedData, config); } async deleteEmployee(id, config) { return this.request("DELETE", `/api/v1/employees/${id}`, void 0, config); } }; //#endregion //#region src/modules/inventory-client.ts var InventoryClient = class extends BaseClient { async getStockReservations(filters, config) { const params = new URLSearchParams(); if (filters) Object.entries(filters).forEach(([key, value]) => { if (value !== void 0 && value !== null) if (Array.isArray(value)) value.forEach((v) => para