@prodobit/sdk
Version:
TypeScript SDK for Prodobit API
1,650 lines (1,638 loc) • 159 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",
roleId: "string.uuid",
"message?": "string",
"expiresInDays?": "number >= 1",
"membershipExpiresAt?": "string.date",
"accessLevel?": "'full' | 'limited' | 'read_only'",
"permissions?": "object",
"resourceRestrictions?": "object"
});
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) if (currentToken?.refreshToken) try {
console.log("Attempting token refresh during initialization");
await this.client.refreshToken();
} catch (error) {
console.log("Refresh failed during initialization:", error);
this.setState({ type: "AUTH_LOGOUT" });
return;
}
else {
console.log("No refresh token available, clearing auth state");
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) {
console.log("Auth initialization failed:", 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/utils/cookie-utils.ts
/**
* Cookie utilities for token management
*/
const cookieUtils = {
set(name, value, options = {}) {
if (typeof document === "undefined") return;
const { maxAge, expires, domain, path = "/", secure, sameSite = "lax", httpOnly = false } = options;
let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
if (maxAge !== void 0) cookieString += `; Max-Age=${maxAge}`;
if (expires) cookieString += `; Expires=${expires.toUTCString()}`;
if (domain) cookieString += `; Domain=${domain}`;
cookieString += `; Path=${path}`;
if (secure) cookieString += "; Secure";
cookieString += `; SameSite=${sameSite}`;
if (httpOnly) cookieString += "; HttpOnly";
document.cookie = cookieString;
},
get(name) {
if (typeof document === "undefined") return void 0;
const nameEQ = encodeURIComponent(name) + "=";
const cookies = document.cookie.split(";");
for (let cookie of cookies) {
cookie = cookie.trim();
if (cookie.startsWith(nameEQ)) return decodeURIComponent(cookie.substring(nameEQ.length));
}
},
remove(name, options = {}) {
this.set(name, "", {
...options,
maxAge: 0,
expires: /* @__PURE__ */ new Date(0)
});
},
exists(name) {
return this.get(name) !== void 0;
}
};
/**
* Token-specific cookie management
*/
const tokenCookies = {
ACCESS_TOKEN: "prodobit_access_token",
REFRESH_TOKEN: "prodobit_refresh_token",
CSRF_TOKEN: "prodobit_csrf_token",
TENANT_ID: "prodobit_tenant_id",
setTokens(tokenInfo) {
const isProduction = process.env.NODE_ENV === "production" || typeof window !== "undefined" && window.location.hostname.includes("prodobit.com");
const cookieOptions = {
path: "/",
secure: isProduction,
sameSite: "lax",
httpOnly: false,
domain: isProduction ? ".prodobit.com" : void 0
};
const maxAge = tokenInfo.expiresAt ? Math.max(0, Math.floor((tokenInfo.expiresAt.getTime() - Date.now()) / 1e3)) : 3600 * 24;
if (tokenInfo.accessToken) cookieUtils.set(this.ACCESS_TOKEN, tokenInfo.accessToken, {
...cookieOptions,
maxAge
});
if (tokenInfo.refreshToken) cookieUtils.set(this.REFRESH_TOKEN, tokenInfo.refreshToken, {
...cookieOptions,
maxAge: 3600 * 24 * 30
});
if (tokenInfo.tenantId) cookieUtils.set(this.TENANT_ID, tokenInfo.tenantId, {
...cookieOptions,
maxAge: 3600 * 24 * 30
});
},
getTokens() {
const accessToken = cookieUtils.get(this.ACCESS_TOKEN);
const refreshToken = cookieUtils.get(this.REFRESH_TOKEN);
const tenantId = cookieUtils.get(this.TENANT_ID);
if (!accessToken) return;
let expiresAt;
try {
const payload = JSON.parse(atob(accessToken.split(".")[1]));
if (payload.exp) expiresAt = /* @__PURE__ */ new Date(payload.exp * 1e3);
} catch {
expiresAt = new Date(Date.now() + 3600 * 1e3);
}
return {
accessToken,
refreshToken,
expiresAt: expiresAt || new Date(Date.now() + 3600 * 1e3),
tenantId
};
},
clearTokens() {
const domain = process.env.NODE_ENV === "production" || typeof window !== "undefined" && window.location.hostname.includes("prodobit.com") ? ".prodobit.com" : void 0;
const cookieOptions = {
path: "/",
domain
};
cookieUtils.remove(this.ACCESS_TOKEN, cookieOptions);
cookieUtils.remove(this.REFRESH_TOKEN, cookieOptions);
cookieUtils.remove(this.TENANT_ID, cookieOptions);
if (domain) {
const currentDomainOptions = {
path: "/",
domain: void 0
};
cookieUtils.remove(this.ACCESS_TOKEN, currentDomainOptions);
cookieUtils.remove(this.REFRESH_TOKEN, currentDomainOptions);
cookieUtils.remove(this.TENANT_ID, currentDomainOptions);
}
},
hasValidTokens() {
const accessToken = cookieUtils.get(this.ACCESS_TOKEN);
if (!accessToken) return false;
try {
const payload = JSON.parse(atob(accessToken.split(".")[1]));
if (payload.exp) return /* @__PURE__ */ new Date(payload.exp * 1e3) > /* @__PURE__ */ new Date();
} catch {
return false;
}
return true;
},
getTenantId() {
return cookieUtils.get(this.TENANT_ID);
}
};
//#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.loadTokenFromCookies();
}
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}`;
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: "include"
});
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.refreshToken || this.tokenInfo?.refreshToken,
expiresAt: new Date(response.data.session.expiresAt),
csrfToken: response.data.session.csrfToken,
tenantId: this.tokenInfo?.tenantId
});
} catch (error) {
this.clearTokenInfo();
throw error;
}
}
loadTokenFromCookies() {
if (typeof window === "undefined") return void 0;
try {
const tokenInfo = tokenCookies.getTokens();
if (tokenInfo?.expiresAt && tokenInfo.expiresAt <= /* @__PURE__ */ new Date()) {
this.clearTokenInfo();
return;
}
return tokenInfo;
} catch {
this.clearTokenInfo();
return;
}
}
setTokenInfo(tokenInfo) {
this.tokenInfo = tokenInfo;
if (typeof window !== "undefined") try {
tokenCookies.setTokens(tokenInfo);
} catch (error) {
console.warn("Failed to store tokens in cookies:", error);
}
}
getTokenInfo() {
return this.tokenInfo;
}
clearTokenInfo() {
this.tokenInfo = void 0;
if (typeof window !== "undefined") try {
tokenCookies.clearTokens();
} catch (error) {
console.warn("Failed to clear tokens from cookies:", error);
}
}
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),
isTokenValid: this.isTokenValid.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 in cookies:", {
hasAccessToken: !!response.data.session.accessToken,
hasRefreshToken: !!response.data.refreshToken,
hasCsrfToken: !!response.data.session.csrfToken
});
const tokenInfo = {
accessToken: response.data.session.accessToken,
expiresAt: new Date(response.data.session.expiresAt),
csrfToken: response.data.session.csrfToken,
tenantId
};
if (response.data.refreshToken) tokenInfo.refreshToken = response.data.refreshToken;
this.setTokenInfo(tokenInfo);
}
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 from cookies:", 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 || this.tokenInfo.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() && this.tokenInfo?.refreshToken) try {
console.log("Calling refreshToken from refreshAuthState");
await this.refreshToken();
} catch (error) {
console.log("refreshAuthState failed:", error);
this.clearTokenInfo();
throw error;
}
else console.log("Skipping refresh - no refresh token available");
}
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/manufacturi