@prodobit/sdk
Version:
TypeScript SDK for Prodobit API
1,626 lines (1,612 loc) • 154 kB
JavaScript
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