@clianta/sdk 1.7.1 → 1.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Clianta SDK v1.7.1
2
+ * Clianta SDK v1.7.3
3
3
  * (c) 2026 Clianta
4
4
  * Released under the MIT License.
5
5
  */
@@ -13,7 +13,7 @@ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentS
13
13
  * @see SDK_VERSION in core/config.ts
14
14
  */
15
15
  /** SDK Version */
16
- const SDK_VERSION = '1.7.0';
16
+ const SDK_VERSION = '1.7.2';
17
17
  /** Default API endpoint — reads from env or falls back to localhost */
18
18
  const getDefaultApiEndpoint = () => {
19
19
  // Next.js (process.env)
@@ -224,8 +224,9 @@ class Transport {
224
224
  /**
225
225
  * Send identify request.
226
226
  * Returns contactId from the server response so the Tracker can store it.
227
+ * Retries on 5xx with exponential backoff (same policy as sendEvents).
227
228
  */
228
- async sendIdentify(data) {
229
+ async sendIdentify(data, attempt = 1) {
229
230
  const url = `${this.config.apiEndpoint}/api/public/track/identify`;
230
231
  try {
231
232
  const response = await this.fetchWithTimeout(url, {
@@ -243,16 +244,26 @@ class Transport {
243
244
  contactId: body.contactId ?? undefined,
244
245
  };
245
246
  }
246
- if (response.status >= 500) {
247
- logger.warn(`Identify server error (${response.status})`);
248
- }
249
- else {
250
- logger.error(`Identify failed with status ${response.status}:`, body.message);
247
+ // Server error retry with exponential backoff
248
+ if (response.status >= 500 && attempt < this.config.maxRetries) {
249
+ const backoff = this.config.retryDelay * Math.pow(2, attempt - 1);
250
+ logger.warn(`Identify server error (${response.status}), retrying in ${backoff}ms...`);
251
+ await this.delay(backoff);
252
+ return this.sendIdentify(data, attempt + 1);
251
253
  }
254
+ logger.error(`Identify failed with status ${response.status}:`, body.message);
252
255
  return { success: false, status: response.status };
253
256
  }
254
257
  catch (error) {
255
- logger.error('Identify request failed:', error);
258
+ // Network error retry if still online
259
+ const isOnline = typeof navigator === 'undefined' || navigator.onLine;
260
+ if (isOnline && attempt < this.config.maxRetries) {
261
+ const backoff = this.config.retryDelay * Math.pow(2, attempt - 1);
262
+ logger.warn(`Identify network error, retrying in ${backoff}ms (${attempt}/${this.config.maxRetries})...`);
263
+ await this.delay(backoff);
264
+ return this.sendIdentify(data, attempt + 1);
265
+ }
266
+ logger.error('Identify request failed after retries:', error);
256
267
  return { success: false, error: error };
257
268
  }
258
269
  }
@@ -570,9 +581,6 @@ function resetIds(useCookies = false) {
570
581
  // Ignore
571
582
  }
572
583
  }
573
- // ============================================
574
- // URL UTILITIES
575
- // ============================================
576
584
  /**
577
585
  * Extract UTM parameters from URL
578
586
  */
@@ -729,8 +737,9 @@ class EventQueue {
729
737
  flushInterval: config.flushInterval ?? 5000,
730
738
  maxQueueSize: config.maxQueueSize ?? MAX_QUEUE_SIZE,
731
739
  storageKey: config.storageKey ?? STORAGE_KEYS.EVENT_QUEUE,
740
+ persistMode: config.persistMode ?? 'session',
732
741
  };
733
- this.persistMode = config.persistMode || 'session';
742
+ this.persistMode = this.config.persistMode;
734
743
  this.isOnline = typeof navigator === 'undefined' || navigator.onLine;
735
744
  // Restore persisted queue
736
745
  this.restoreQueue();
@@ -794,9 +803,11 @@ class EventQueue {
794
803
  // Send to backend
795
804
  const result = await this.transport.sendEvents(events);
796
805
  if (!result.success) {
797
- // Re-queue events on failure (at the front)
806
+ // Re-queue events on failure (at the front), capped at maxQueueSize
798
807
  logger.warn('Flush failed, re-queuing events');
799
- this.queue.unshift(...events);
808
+ const availableSpace = this.config.maxQueueSize - this.queue.length;
809
+ const eventsToRequeue = events.slice(0, Math.max(0, availableSpace));
810
+ this.queue.unshift(...eventsToRequeue);
800
811
  this.persistQueue(this.queue);
801
812
  }
802
813
  else {
@@ -1243,7 +1254,7 @@ class FormsPlugin extends BasePlugin {
1243
1254
  if (this.trackedForms.has(form))
1244
1255
  return;
1245
1256
  this.trackedForms.add(form);
1246
- const formId = form.id || form.name || `form-${Math.random().toString(36).substr(2, 9)}`;
1257
+ const formId = form.id || form.name || `form-${Math.random().toString(36).substring(2, 11)}`;
1247
1258
  // Track form view
1248
1259
  this.track('form_view', 'Form Viewed', {
1249
1260
  formId,
@@ -1887,27 +1898,22 @@ class PopupFormsPlugin extends BasePlugin {
1887
1898
  super.destroy();
1888
1899
  }
1889
1900
  loadShownForms() {
1890
- try {
1891
- const stored = localStorage.getItem('clianta_shown_forms');
1892
- if (stored) {
1901
+ const stored = getLocalStorage('clianta_shown_forms');
1902
+ if (stored) {
1903
+ try {
1893
1904
  const data = JSON.parse(stored);
1894
1905
  this.shownForms = new Set(data.forms || []);
1895
1906
  }
1896
- }
1897
- catch (e) {
1898
- // Ignore storage errors
1907
+ catch {
1908
+ // Ignore parse errors
1909
+ }
1899
1910
  }
1900
1911
  }
1901
1912
  saveShownForms() {
1902
- try {
1903
- localStorage.setItem('clianta_shown_forms', JSON.stringify({
1904
- forms: Array.from(this.shownForms),
1905
- timestamp: Date.now(),
1906
- }));
1907
- }
1908
- catch (e) {
1909
- // Ignore storage errors
1910
- }
1913
+ setLocalStorage('clianta_shown_forms', JSON.stringify({
1914
+ forms: Array.from(this.shownForms),
1915
+ timestamp: Date.now(),
1916
+ }));
1911
1917
  }
1912
1918
  async fetchForms() {
1913
1919
  if (!this.tracker)
@@ -1926,7 +1932,7 @@ class PopupFormsPlugin extends BasePlugin {
1926
1932
  }
1927
1933
  }
1928
1934
  catch (error) {
1929
- console.error('[Clianta] Failed to fetch forms:', error);
1935
+ logger.error('Failed to fetch popup forms:', error);
1930
1936
  }
1931
1937
  }
1932
1938
  shouldShowForm(form) {
@@ -1937,7 +1943,7 @@ class PopupFormsPlugin extends BasePlugin {
1937
1943
  }
1938
1944
  else if (form.showFrequency === 'once_per_session') {
1939
1945
  const sessionKey = `clianta_form_${form._id}_shown`;
1940
- if (sessionStorage.getItem(sessionKey))
1946
+ if (getSessionStorage(sessionKey))
1941
1947
  return false;
1942
1948
  }
1943
1949
  return true;
@@ -2009,7 +2015,7 @@ class PopupFormsPlugin extends BasePlugin {
2009
2015
  // Mark as shown
2010
2016
  this.shownForms.add(form._id);
2011
2017
  this.saveShownForms();
2012
- sessionStorage.setItem(`clianta_form_${form._id}_shown`, 'true');
2018
+ setSessionStorage(`clianta_form_${form._id}_shown`, 'true');
2013
2019
  // Track view
2014
2020
  await this.trackFormView(form._id);
2015
2021
  // Render form
@@ -2325,17 +2331,17 @@ class PopupFormsPlugin extends BasePlugin {
2325
2331
  const redirect = new URL(form.redirectUrl, window.location.origin);
2326
2332
  const isSameOrigin = redirect.origin === window.location.origin;
2327
2333
  const isSafeProtocol = redirect.protocol === 'https:' || redirect.protocol === 'http:';
2328
- if (isSameOrigin || isSafeProtocol) {
2334
+ if (isSameOrigin && isSafeProtocol) {
2329
2335
  setTimeout(() => {
2330
2336
  window.location.href = redirect.href;
2331
2337
  }, 1500);
2332
2338
  }
2333
2339
  else {
2334
- console.warn('[Clianta] Blocked unsafe redirect URL:', form.redirectUrl);
2340
+ logger.warn('Blocked unsafe redirect URL:', form.redirectUrl);
2335
2341
  }
2336
2342
  }
2337
2343
  catch {
2338
- console.warn('[Clianta] Invalid redirect URL:', form.redirectUrl);
2344
+ logger.warn('Invalid redirect URL:', form.redirectUrl);
2339
2345
  }
2340
2346
  }
2341
2347
  // Close after delay
@@ -2348,7 +2354,7 @@ class PopupFormsPlugin extends BasePlugin {
2348
2354
  }
2349
2355
  }
2350
2356
  catch (error) {
2351
- console.error('[Clianta] Form submit error:', error);
2357
+ logger.error('Form submit error:', error);
2352
2358
  if (submitBtn) {
2353
2359
  submitBtn.disabled = false;
2354
2360
  submitBtn.textContent = form.submitButtonText || 'Subscribe';
@@ -2419,7 +2425,7 @@ const STORAGE_KEY_PATTERNS = [
2419
2425
  'token', 'jwt', 'auth', 'user', 'session', 'credential', 'account',
2420
2426
  ];
2421
2427
  /** JWT/user object fields containing email */
2422
- const EMAIL_CLAIMS = ['email', 'sub', 'preferred_username', 'user_email', 'mail', 'emailAddress', 'e_mail'];
2428
+ const EMAIL_CLAIMS = ['email', 'preferred_username', 'user_email', 'mail', 'emailAddress', 'e_mail'];
2423
2429
  /** Full name fields */
2424
2430
  const NAME_CLAIMS = ['name', 'full_name', 'display_name', 'displayName'];
2425
2431
  /** First name fields */
@@ -3363,6 +3369,7 @@ class Tracker {
3363
3369
  this.queue = new EventQueue(this.transport, {
3364
3370
  batchSize: this.config.batchSize,
3365
3371
  flushInterval: this.config.flushInterval,
3372
+ persistMode: this.config.persistMode,
3366
3373
  });
3367
3374
  // Get or create visitor and session IDs based on mode
3368
3375
  this.visitorId = this.createVisitorId();
@@ -3479,10 +3486,13 @@ class Tracker {
3479
3486
  logger.warn('SDK not initialized, event dropped');
3480
3487
  return;
3481
3488
  }
3489
+ const utmParams = getUTMParams();
3482
3490
  const event = {
3483
3491
  workspaceId: this.workspaceId,
3484
3492
  visitorId: this.visitorId,
3485
3493
  sessionId: this.sessionId,
3494
+ contactId: this.contactId ?? undefined,
3495
+ groupId: this.groupId ?? undefined,
3486
3496
  eventType: eventType,
3487
3497
  eventName,
3488
3498
  url: typeof window !== 'undefined' ? window.location.href : '',
@@ -3493,18 +3503,14 @@ class Tracker {
3493
3503
  websiteDomain: typeof window !== 'undefined' ? window.location.hostname : undefined,
3494
3504
  },
3495
3505
  device: getDeviceInfo(),
3496
- ...getUTMParams(),
3506
+ utmSource: utmParams.utmSource,
3507
+ utmMedium: utmParams.utmMedium,
3508
+ utmCampaign: utmParams.utmCampaign,
3509
+ utmTerm: utmParams.utmTerm,
3510
+ utmContent: utmParams.utmContent,
3497
3511
  timestamp: new Date().toISOString(),
3498
3512
  sdkVersion: SDK_VERSION,
3499
3513
  };
3500
- // Attach contactId if known (from a prior identify() call)
3501
- if (this.contactId) {
3502
- event.contactId = this.contactId;
3503
- }
3504
- // Attach groupId if known (from a prior group() call)
3505
- if (this.groupId) {
3506
- event.groupId = this.groupId;
3507
- }
3508
3514
  // Validate event against registered schema (debug mode only)
3509
3515
  this.validateEventSchema(eventType, properties);
3510
3516
  // Check consent before tracking
@@ -3965,6 +3971,1189 @@ class Tracker {
3965
3971
  }
3966
3972
  }
3967
3973
 
3974
+ /**
3975
+ * Clianta SDK - Event Triggers Manager
3976
+ * Manages event-driven automation and email notifications
3977
+ */
3978
+ /**
3979
+ * Event Triggers Manager
3980
+ * Handles event-driven automation based on CRM actions
3981
+ *
3982
+ * Similar to:
3983
+ * - Salesforce: Process Builder, Flow Automation
3984
+ * - HubSpot: Workflows, Email Sequences
3985
+ * - Pipedrive: Workflow Automation
3986
+ */
3987
+ class EventTriggersManager {
3988
+ constructor(apiEndpoint, workspaceId, authToken) {
3989
+ this.triggers = new Map();
3990
+ this.listeners = new Map();
3991
+ this.apiEndpoint = apiEndpoint;
3992
+ this.workspaceId = workspaceId;
3993
+ this.authToken = authToken;
3994
+ }
3995
+ /**
3996
+ * Set authentication token
3997
+ */
3998
+ setAuthToken(token) {
3999
+ this.authToken = token;
4000
+ }
4001
+ /**
4002
+ * Make authenticated API request
4003
+ */
4004
+ async request(endpoint, options = {}) {
4005
+ const url = `${this.apiEndpoint}${endpoint}`;
4006
+ const headers = {
4007
+ 'Content-Type': 'application/json',
4008
+ ...(options.headers || {}),
4009
+ };
4010
+ if (this.authToken) {
4011
+ headers['Authorization'] = `Bearer ${this.authToken}`;
4012
+ }
4013
+ try {
4014
+ const response = await fetch(url, {
4015
+ ...options,
4016
+ headers,
4017
+ });
4018
+ const data = await response.json();
4019
+ if (!response.ok) {
4020
+ return {
4021
+ success: false,
4022
+ error: data.message || 'Request failed',
4023
+ status: response.status,
4024
+ };
4025
+ }
4026
+ return {
4027
+ success: true,
4028
+ data: data.data || data,
4029
+ status: response.status,
4030
+ };
4031
+ }
4032
+ catch (error) {
4033
+ return {
4034
+ success: false,
4035
+ error: error instanceof Error ? error.message : 'Network error',
4036
+ status: 0,
4037
+ };
4038
+ }
4039
+ }
4040
+ // ============================================
4041
+ // TRIGGER MANAGEMENT
4042
+ // ============================================
4043
+ /**
4044
+ * Get all event triggers
4045
+ */
4046
+ async getTriggers() {
4047
+ return this.request(`/api/workspaces/${this.workspaceId}/triggers`);
4048
+ }
4049
+ /**
4050
+ * Get a single trigger by ID
4051
+ */
4052
+ async getTrigger(triggerId) {
4053
+ return this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`);
4054
+ }
4055
+ /**
4056
+ * Create a new event trigger
4057
+ */
4058
+ async createTrigger(trigger) {
4059
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers`, {
4060
+ method: 'POST',
4061
+ body: JSON.stringify(trigger),
4062
+ });
4063
+ // Cache the trigger locally if successful
4064
+ if (result.success && result.data?._id) {
4065
+ this.triggers.set(result.data._id, result.data);
4066
+ }
4067
+ return result;
4068
+ }
4069
+ /**
4070
+ * Update an existing trigger
4071
+ */
4072
+ async updateTrigger(triggerId, updates) {
4073
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
4074
+ method: 'PUT',
4075
+ body: JSON.stringify(updates),
4076
+ });
4077
+ // Update cache if successful
4078
+ if (result.success && result.data?._id) {
4079
+ this.triggers.set(result.data._id, result.data);
4080
+ }
4081
+ return result;
4082
+ }
4083
+ /**
4084
+ * Delete a trigger
4085
+ */
4086
+ async deleteTrigger(triggerId) {
4087
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
4088
+ method: 'DELETE',
4089
+ });
4090
+ // Remove from cache if successful
4091
+ if (result.success) {
4092
+ this.triggers.delete(triggerId);
4093
+ }
4094
+ return result;
4095
+ }
4096
+ /**
4097
+ * Activate a trigger
4098
+ */
4099
+ async activateTrigger(triggerId) {
4100
+ return this.updateTrigger(triggerId, { isActive: true });
4101
+ }
4102
+ /**
4103
+ * Deactivate a trigger
4104
+ */
4105
+ async deactivateTrigger(triggerId) {
4106
+ return this.updateTrigger(triggerId, { isActive: false });
4107
+ }
4108
+ // ============================================
4109
+ // EVENT HANDLING (CLIENT-SIDE)
4110
+ // ============================================
4111
+ /**
4112
+ * Register a local event listener for client-side triggers
4113
+ * This allows immediate client-side reactions to events
4114
+ */
4115
+ on(eventType, callback) {
4116
+ if (!this.listeners.has(eventType)) {
4117
+ this.listeners.set(eventType, new Set());
4118
+ }
4119
+ this.listeners.get(eventType).add(callback);
4120
+ logger.debug(`Event listener registered: ${eventType}`);
4121
+ }
4122
+ /**
4123
+ * Remove an event listener
4124
+ */
4125
+ off(eventType, callback) {
4126
+ const listeners = this.listeners.get(eventType);
4127
+ if (listeners) {
4128
+ listeners.delete(callback);
4129
+ }
4130
+ }
4131
+ /**
4132
+ * Emit an event (client-side only)
4133
+ * This will trigger any registered local listeners
4134
+ */
4135
+ emit(eventType, data) {
4136
+ logger.debug(`Event emitted: ${eventType}`, data);
4137
+ const listeners = this.listeners.get(eventType);
4138
+ if (listeners) {
4139
+ listeners.forEach(callback => {
4140
+ try {
4141
+ callback(data);
4142
+ }
4143
+ catch (error) {
4144
+ logger.error(`Error in event listener for ${eventType}:`, error);
4145
+ }
4146
+ });
4147
+ }
4148
+ }
4149
+ /**
4150
+ * Check if conditions are met for a trigger
4151
+ * Supports dynamic field evaluation including custom fields and nested paths
4152
+ */
4153
+ evaluateConditions(conditions, data) {
4154
+ if (!conditions || conditions.length === 0) {
4155
+ return true; // No conditions means always fire
4156
+ }
4157
+ return conditions.every(condition => {
4158
+ // Support dot notation for nested fields (e.g., 'customFields.industry')
4159
+ const fieldValue = condition.field.includes('.')
4160
+ ? this.getNestedValue(data, condition.field)
4161
+ : data[condition.field];
4162
+ const targetValue = condition.value;
4163
+ switch (condition.operator) {
4164
+ case 'equals':
4165
+ return fieldValue === targetValue;
4166
+ case 'not_equals':
4167
+ return fieldValue !== targetValue;
4168
+ case 'contains':
4169
+ return String(fieldValue).includes(String(targetValue));
4170
+ case 'greater_than':
4171
+ return Number(fieldValue) > Number(targetValue);
4172
+ case 'less_than':
4173
+ return Number(fieldValue) < Number(targetValue);
4174
+ case 'in':
4175
+ return Array.isArray(targetValue) && targetValue.includes(fieldValue);
4176
+ case 'not_in':
4177
+ return Array.isArray(targetValue) && !targetValue.includes(fieldValue);
4178
+ default:
4179
+ return false;
4180
+ }
4181
+ });
4182
+ }
4183
+ /**
4184
+ * Execute actions for a triggered event (client-side preview)
4185
+ * Note: Actual execution happens on the backend
4186
+ */
4187
+ async executeActions(trigger, data) {
4188
+ logger.info(`Executing actions for trigger: ${trigger.name}`);
4189
+ for (const action of trigger.actions) {
4190
+ try {
4191
+ await this.executeAction(action, data);
4192
+ }
4193
+ catch (error) {
4194
+ logger.error(`Failed to execute action:`, error);
4195
+ }
4196
+ }
4197
+ }
4198
+ /**
4199
+ * Execute a single action
4200
+ */
4201
+ async executeAction(action, data) {
4202
+ switch (action.type) {
4203
+ case 'send_email':
4204
+ await this.executeSendEmail(action, data);
4205
+ break;
4206
+ case 'webhook':
4207
+ await this.executeWebhook(action, data);
4208
+ break;
4209
+ case 'create_task':
4210
+ await this.executeCreateTask(action, data);
4211
+ break;
4212
+ case 'update_contact':
4213
+ await this.executeUpdateContact(action, data);
4214
+ break;
4215
+ default:
4216
+ logger.warn(`Unknown action type:`, action);
4217
+ }
4218
+ }
4219
+ /**
4220
+ * Execute send email action (via backend API)
4221
+ */
4222
+ async executeSendEmail(action, data) {
4223
+ logger.debug('Sending email:', action);
4224
+ const payload = {
4225
+ to: this.replaceVariables(action.to, data),
4226
+ subject: action.subject ? this.replaceVariables(action.subject, data) : undefined,
4227
+ body: action.body ? this.replaceVariables(action.body, data) : undefined,
4228
+ templateId: action.templateId,
4229
+ cc: action.cc,
4230
+ bcc: action.bcc,
4231
+ from: action.from,
4232
+ delayMinutes: action.delayMinutes,
4233
+ };
4234
+ await this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
4235
+ method: 'POST',
4236
+ body: JSON.stringify(payload),
4237
+ });
4238
+ }
4239
+ /**
4240
+ * Execute webhook action
4241
+ */
4242
+ async executeWebhook(action, data) {
4243
+ logger.debug('Calling webhook:', action.url);
4244
+ const body = action.body ? this.replaceVariables(action.body, data) : JSON.stringify(data);
4245
+ await fetch(action.url, {
4246
+ method: action.method,
4247
+ headers: {
4248
+ 'Content-Type': 'application/json',
4249
+ ...action.headers,
4250
+ },
4251
+ body,
4252
+ });
4253
+ }
4254
+ /**
4255
+ * Execute create task action
4256
+ */
4257
+ async executeCreateTask(action, data) {
4258
+ logger.debug('Creating task:', action.title);
4259
+ const dueDate = action.dueDays
4260
+ ? new Date(Date.now() + action.dueDays * 24 * 60 * 60 * 1000).toISOString()
4261
+ : undefined;
4262
+ await this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
4263
+ method: 'POST',
4264
+ body: JSON.stringify({
4265
+ title: this.replaceVariables(action.title, data),
4266
+ description: action.description ? this.replaceVariables(action.description, data) : undefined,
4267
+ priority: action.priority,
4268
+ dueDate,
4269
+ assignedTo: action.assignedTo,
4270
+ relatedContactId: typeof data.contactId === 'string' ? data.contactId : undefined,
4271
+ }),
4272
+ });
4273
+ }
4274
+ /**
4275
+ * Execute update contact action
4276
+ */
4277
+ async executeUpdateContact(action, data) {
4278
+ const contactId = data.contactId || data._id;
4279
+ if (!contactId) {
4280
+ logger.warn('Cannot update contact: no contactId in data');
4281
+ return;
4282
+ }
4283
+ logger.debug('Updating contact:', contactId);
4284
+ await this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
4285
+ method: 'PUT',
4286
+ body: JSON.stringify(action.updates),
4287
+ });
4288
+ }
4289
+ /**
4290
+ * Replace variables in a string template
4291
+ * Supports syntax like {{contact.email}}, {{opportunity.value}}
4292
+ */
4293
+ replaceVariables(template, data) {
4294
+ return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
4295
+ const value = this.getNestedValue(data, path.trim());
4296
+ return value !== undefined ? String(value) : match;
4297
+ });
4298
+ }
4299
+ /**
4300
+ * Get nested value from object using dot notation
4301
+ * Supports dynamic field access including custom fields
4302
+ */
4303
+ getNestedValue(obj, path) {
4304
+ return path.split('.').reduce((current, key) => {
4305
+ return current !== null && current !== undefined && typeof current === 'object'
4306
+ ? current[key]
4307
+ : undefined;
4308
+ }, obj);
4309
+ }
4310
+ /**
4311
+ * Extract all available field paths from a data object
4312
+ * Useful for dynamic field discovery based on platform-specific attributes
4313
+ * @param obj - The data object to extract fields from
4314
+ * @param prefix - Internal use for nested paths
4315
+ * @param maxDepth - Maximum depth to traverse (default: 3)
4316
+ * @returns Array of field paths (e.g., ['email', 'contact.firstName', 'customFields.industry'])
4317
+ */
4318
+ extractAvailableFields(obj, prefix = '', maxDepth = 3) {
4319
+ if (maxDepth <= 0)
4320
+ return [];
4321
+ const fields = [];
4322
+ for (const key in obj) {
4323
+ if (!obj.hasOwnProperty(key))
4324
+ continue;
4325
+ const value = obj[key];
4326
+ const fieldPath = prefix ? `${prefix}.${key}` : key;
4327
+ fields.push(fieldPath);
4328
+ // Recursively traverse nested objects
4329
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
4330
+ const nestedFields = this.extractAvailableFields(value, fieldPath, maxDepth - 1);
4331
+ fields.push(...nestedFields);
4332
+ }
4333
+ }
4334
+ return fields;
4335
+ }
4336
+ /**
4337
+ * Get available fields from sample data
4338
+ * Helps with dynamic field detection for platform-specific attributes
4339
+ * @param sampleData - Sample data object to analyze
4340
+ * @returns Array of available field paths
4341
+ */
4342
+ getAvailableFields(sampleData) {
4343
+ return this.extractAvailableFields(sampleData);
4344
+ }
4345
+ // ============================================
4346
+ // HELPER METHODS FOR COMMON PATTERNS
4347
+ // ============================================
4348
+ /**
4349
+ * Create a simple email trigger
4350
+ * Helper method for common use case
4351
+ */
4352
+ async createEmailTrigger(config) {
4353
+ return this.createTrigger({
4354
+ name: config.name,
4355
+ eventType: config.eventType,
4356
+ conditions: config.conditions,
4357
+ actions: [
4358
+ {
4359
+ type: 'send_email',
4360
+ to: config.to,
4361
+ subject: config.subject,
4362
+ body: config.body,
4363
+ },
4364
+ ],
4365
+ isActive: true,
4366
+ });
4367
+ }
4368
+ /**
4369
+ * Create a task creation trigger
4370
+ */
4371
+ async createTaskTrigger(config) {
4372
+ return this.createTrigger({
4373
+ name: config.name,
4374
+ eventType: config.eventType,
4375
+ conditions: config.conditions,
4376
+ actions: [
4377
+ {
4378
+ type: 'create_task',
4379
+ title: config.taskTitle,
4380
+ description: config.taskDescription,
4381
+ priority: config.priority,
4382
+ dueDays: config.dueDays,
4383
+ },
4384
+ ],
4385
+ isActive: true,
4386
+ });
4387
+ }
4388
+ /**
4389
+ * Create a webhook trigger
4390
+ */
4391
+ async createWebhookTrigger(config) {
4392
+ return this.createTrigger({
4393
+ name: config.name,
4394
+ eventType: config.eventType,
4395
+ conditions: config.conditions,
4396
+ actions: [
4397
+ {
4398
+ type: 'webhook',
4399
+ url: config.webhookUrl,
4400
+ method: config.method || 'POST',
4401
+ },
4402
+ ],
4403
+ isActive: true,
4404
+ });
4405
+ }
4406
+ }
4407
+
4408
+ /**
4409
+ * Clianta SDK - CRM API Client
4410
+ * @see SDK_VERSION in core/config.ts
4411
+ */
4412
+ /**
4413
+ * CRM API Client for managing contacts and opportunities
4414
+ */
4415
+ class CRMClient {
4416
+ constructor(apiEndpoint, workspaceId, authToken, apiKey) {
4417
+ this.apiEndpoint = apiEndpoint;
4418
+ this.workspaceId = workspaceId;
4419
+ this.authToken = authToken;
4420
+ this.apiKey = apiKey;
4421
+ this.triggers = new EventTriggersManager(apiEndpoint, workspaceId, authToken);
4422
+ }
4423
+ /**
4424
+ * Set authentication token for API requests (user JWT)
4425
+ */
4426
+ setAuthToken(token) {
4427
+ this.authToken = token;
4428
+ this.apiKey = undefined;
4429
+ this.triggers.setAuthToken(token);
4430
+ }
4431
+ /**
4432
+ * Set workspace API key for server-to-server requests.
4433
+ * Use this instead of setAuthToken when integrating from an external app.
4434
+ */
4435
+ setApiKey(key) {
4436
+ this.apiKey = key;
4437
+ this.authToken = undefined;
4438
+ }
4439
+ /**
4440
+ * Validate required parameter exists
4441
+ * @throws {Error} if value is null/undefined or empty string
4442
+ */
4443
+ validateRequired(param, value, methodName) {
4444
+ if (value === null || value === undefined || value === '') {
4445
+ throw new Error(`[CRMClient.${methodName}] ${param} is required`);
4446
+ }
4447
+ }
4448
+ /**
4449
+ * Make authenticated API request
4450
+ */
4451
+ async request(endpoint, options = {}) {
4452
+ const url = `${this.apiEndpoint}${endpoint}`;
4453
+ const headers = {
4454
+ 'Content-Type': 'application/json',
4455
+ ...(options.headers || {}),
4456
+ };
4457
+ if (this.apiKey) {
4458
+ headers['X-Api-Key'] = this.apiKey;
4459
+ }
4460
+ else if (this.authToken) {
4461
+ headers['Authorization'] = `Bearer ${this.authToken}`;
4462
+ }
4463
+ try {
4464
+ const response = await fetch(url, {
4465
+ ...options,
4466
+ headers,
4467
+ });
4468
+ const data = await response.json();
4469
+ if (!response.ok) {
4470
+ return {
4471
+ success: false,
4472
+ error: data.message || 'Request failed',
4473
+ status: response.status,
4474
+ };
4475
+ }
4476
+ return {
4477
+ success: true,
4478
+ data: data.data || data,
4479
+ status: response.status,
4480
+ };
4481
+ }
4482
+ catch (error) {
4483
+ return {
4484
+ success: false,
4485
+ error: error instanceof Error ? error.message : 'Network error',
4486
+ status: 0,
4487
+ };
4488
+ }
4489
+ }
4490
+ // ============================================
4491
+ // INBOUND EVENTS API (API-key authenticated)
4492
+ // ============================================
4493
+ /**
4494
+ * Send an inbound event from an external app (e.g. user signup on client website).
4495
+ * Requires the client to be initialized with an API key via setApiKey() or the constructor.
4496
+ *
4497
+ * The contact is upserted in the CRM and matching workflow automations fire automatically.
4498
+ *
4499
+ * @example
4500
+ * const crm = new CRMClient('http://localhost:5000', 'WORKSPACE_ID');
4501
+ * crm.setApiKey('mm_live_...');
4502
+ *
4503
+ * await crm.sendEvent({
4504
+ * event: 'user.registered',
4505
+ * contact: { email: 'alice@example.com', firstName: 'Alice' },
4506
+ * data: { plan: 'free', signupSource: 'homepage' },
4507
+ * });
4508
+ */
4509
+ async sendEvent(payload) {
4510
+ const url = `${this.apiEndpoint}/api/public/events`;
4511
+ const headers = { 'Content-Type': 'application/json' };
4512
+ if (this.apiKey) {
4513
+ headers['X-Api-Key'] = this.apiKey;
4514
+ }
4515
+ else if (this.authToken) {
4516
+ headers['Authorization'] = `Bearer ${this.authToken}`;
4517
+ }
4518
+ try {
4519
+ const response = await fetch(url, {
4520
+ method: 'POST',
4521
+ headers,
4522
+ body: JSON.stringify(payload),
4523
+ });
4524
+ const data = await response.json();
4525
+ if (!response.ok) {
4526
+ return {
4527
+ success: false,
4528
+ contactCreated: false,
4529
+ event: payload.event,
4530
+ error: data.error || 'Request failed',
4531
+ };
4532
+ }
4533
+ return {
4534
+ success: data.success,
4535
+ contactCreated: data.contactCreated,
4536
+ contactId: data.contactId,
4537
+ event: data.event,
4538
+ };
4539
+ }
4540
+ catch (error) {
4541
+ return {
4542
+ success: false,
4543
+ contactCreated: false,
4544
+ event: payload.event,
4545
+ error: error instanceof Error ? error.message : 'Network error',
4546
+ };
4547
+ }
4548
+ }
4549
+ // ============================================
4550
+ // CONTACTS API
4551
+ // ============================================
4552
+ /**
4553
+ * Get all contacts with pagination
4554
+ */
4555
+ async getContacts(params) {
4556
+ const queryParams = new URLSearchParams();
4557
+ if (params?.page)
4558
+ queryParams.set('page', params.page.toString());
4559
+ if (params?.limit)
4560
+ queryParams.set('limit', params.limit.toString());
4561
+ if (params?.search)
4562
+ queryParams.set('search', params.search);
4563
+ if (params?.status)
4564
+ queryParams.set('status', params.status);
4565
+ const query = queryParams.toString();
4566
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts${query ? `?${query}` : ''}`;
4567
+ return this.request(endpoint);
4568
+ }
4569
+ /**
4570
+ * Get a single contact by ID
4571
+ */
4572
+ async getContact(contactId) {
4573
+ this.validateRequired('contactId', contactId, 'getContact');
4574
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`);
4575
+ }
4576
+ /**
4577
+ * Create a new contact
4578
+ */
4579
+ async createContact(contact) {
4580
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts`, {
4581
+ method: 'POST',
4582
+ body: JSON.stringify(contact),
4583
+ });
4584
+ }
4585
+ /**
4586
+ * Update an existing contact
4587
+ */
4588
+ async updateContact(contactId, updates) {
4589
+ this.validateRequired('contactId', contactId, 'updateContact');
4590
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
4591
+ method: 'PUT',
4592
+ body: JSON.stringify(updates),
4593
+ });
4594
+ }
4595
+ /**
4596
+ * Delete a contact
4597
+ */
4598
+ async deleteContact(contactId) {
4599
+ this.validateRequired('contactId', contactId, 'deleteContact');
4600
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
4601
+ method: 'DELETE',
4602
+ });
4603
+ }
4604
+ // ============================================
4605
+ // OPPORTUNITIES API
4606
+ // ============================================
4607
+ /**
4608
+ * Get all opportunities with pagination
4609
+ */
4610
+ async getOpportunities(params) {
4611
+ const queryParams = new URLSearchParams();
4612
+ if (params?.page)
4613
+ queryParams.set('page', params.page.toString());
4614
+ if (params?.limit)
4615
+ queryParams.set('limit', params.limit.toString());
4616
+ if (params?.pipelineId)
4617
+ queryParams.set('pipelineId', params.pipelineId);
4618
+ if (params?.stageId)
4619
+ queryParams.set('stageId', params.stageId);
4620
+ const query = queryParams.toString();
4621
+ const endpoint = `/api/workspaces/${this.workspaceId}/opportunities${query ? `?${query}` : ''}`;
4622
+ return this.request(endpoint);
4623
+ }
4624
+ /**
4625
+ * Get a single opportunity by ID
4626
+ */
4627
+ async getOpportunity(opportunityId) {
4628
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`);
4629
+ }
4630
+ /**
4631
+ * Create a new opportunity
4632
+ */
4633
+ async createOpportunity(opportunity) {
4634
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities`, {
4635
+ method: 'POST',
4636
+ body: JSON.stringify(opportunity),
4637
+ });
4638
+ }
4639
+ /**
4640
+ * Update an existing opportunity
4641
+ */
4642
+ async updateOpportunity(opportunityId, updates) {
4643
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
4644
+ method: 'PUT',
4645
+ body: JSON.stringify(updates),
4646
+ });
4647
+ }
4648
+ /**
4649
+ * Delete an opportunity
4650
+ */
4651
+ async deleteOpportunity(opportunityId) {
4652
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
4653
+ method: 'DELETE',
4654
+ });
4655
+ }
4656
+ /**
4657
+ * Move opportunity to a different stage
4658
+ */
4659
+ async moveOpportunity(opportunityId, stageId) {
4660
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/move`, {
4661
+ method: 'POST',
4662
+ body: JSON.stringify({ stageId }),
4663
+ });
4664
+ }
4665
+ // ============================================
4666
+ // COMPANIES API
4667
+ // ============================================
4668
+ /**
4669
+ * Get all companies with pagination
4670
+ */
4671
+ async getCompanies(params) {
4672
+ const queryParams = new URLSearchParams();
4673
+ if (params?.page)
4674
+ queryParams.set('page', params.page.toString());
4675
+ if (params?.limit)
4676
+ queryParams.set('limit', params.limit.toString());
4677
+ if (params?.search)
4678
+ queryParams.set('search', params.search);
4679
+ if (params?.status)
4680
+ queryParams.set('status', params.status);
4681
+ if (params?.industry)
4682
+ queryParams.set('industry', params.industry);
4683
+ const query = queryParams.toString();
4684
+ const endpoint = `/api/workspaces/${this.workspaceId}/companies${query ? `?${query}` : ''}`;
4685
+ return this.request(endpoint);
4686
+ }
4687
+ /**
4688
+ * Get a single company by ID
4689
+ */
4690
+ async getCompany(companyId) {
4691
+ return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`);
4692
+ }
4693
+ /**
4694
+ * Create a new company
4695
+ */
4696
+ async createCompany(company) {
4697
+ return this.request(`/api/workspaces/${this.workspaceId}/companies`, {
4698
+ method: 'POST',
4699
+ body: JSON.stringify(company),
4700
+ });
4701
+ }
4702
+ /**
4703
+ * Update an existing company
4704
+ */
4705
+ async updateCompany(companyId, updates) {
4706
+ return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
4707
+ method: 'PUT',
4708
+ body: JSON.stringify(updates),
4709
+ });
4710
+ }
4711
+ /**
4712
+ * Delete a company
4713
+ */
4714
+ async deleteCompany(companyId) {
4715
+ return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
4716
+ method: 'DELETE',
4717
+ });
4718
+ }
4719
+ /**
4720
+ * Get contacts belonging to a company
4721
+ */
4722
+ async getCompanyContacts(companyId, params) {
4723
+ const queryParams = new URLSearchParams();
4724
+ if (params?.page)
4725
+ queryParams.set('page', params.page.toString());
4726
+ if (params?.limit)
4727
+ queryParams.set('limit', params.limit.toString());
4728
+ const query = queryParams.toString();
4729
+ const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/contacts${query ? `?${query}` : ''}`;
4730
+ return this.request(endpoint);
4731
+ }
4732
+ /**
4733
+ * Get deals/opportunities belonging to a company
4734
+ */
4735
+ async getCompanyDeals(companyId, params) {
4736
+ const queryParams = new URLSearchParams();
4737
+ if (params?.page)
4738
+ queryParams.set('page', params.page.toString());
4739
+ if (params?.limit)
4740
+ queryParams.set('limit', params.limit.toString());
4741
+ const query = queryParams.toString();
4742
+ const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/deals${query ? `?${query}` : ''}`;
4743
+ return this.request(endpoint);
4744
+ }
4745
+ // ============================================
4746
+ // PIPELINES API
4747
+ // ============================================
4748
+ /**
4749
+ * Get all pipelines
4750
+ */
4751
+ async getPipelines() {
4752
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines`);
4753
+ }
4754
+ /**
4755
+ * Get a single pipeline by ID
4756
+ */
4757
+ async getPipeline(pipelineId) {
4758
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`);
4759
+ }
4760
+ /**
4761
+ * Create a new pipeline
4762
+ */
4763
+ async createPipeline(pipeline) {
4764
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines`, {
4765
+ method: 'POST',
4766
+ body: JSON.stringify(pipeline),
4767
+ });
4768
+ }
4769
+ /**
4770
+ * Update an existing pipeline
4771
+ */
4772
+ async updatePipeline(pipelineId, updates) {
4773
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
4774
+ method: 'PUT',
4775
+ body: JSON.stringify(updates),
4776
+ });
4777
+ }
4778
+ /**
4779
+ * Delete a pipeline
4780
+ */
4781
+ async deletePipeline(pipelineId) {
4782
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
4783
+ method: 'DELETE',
4784
+ });
4785
+ }
4786
+ // ============================================
4787
+ // TASKS API
4788
+ // ============================================
4789
+ /**
4790
+ * Get all tasks with pagination
4791
+ */
4792
+ async getTasks(params) {
4793
+ const queryParams = new URLSearchParams();
4794
+ if (params?.page)
4795
+ queryParams.set('page', params.page.toString());
4796
+ if (params?.limit)
4797
+ queryParams.set('limit', params.limit.toString());
4798
+ if (params?.status)
4799
+ queryParams.set('status', params.status);
4800
+ if (params?.priority)
4801
+ queryParams.set('priority', params.priority);
4802
+ if (params?.contactId)
4803
+ queryParams.set('contactId', params.contactId);
4804
+ if (params?.companyId)
4805
+ queryParams.set('companyId', params.companyId);
4806
+ if (params?.opportunityId)
4807
+ queryParams.set('opportunityId', params.opportunityId);
4808
+ const query = queryParams.toString();
4809
+ const endpoint = `/api/workspaces/${this.workspaceId}/tasks${query ? `?${query}` : ''}`;
4810
+ return this.request(endpoint);
4811
+ }
4812
+ /**
4813
+ * Get a single task by ID
4814
+ */
4815
+ async getTask(taskId) {
4816
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`);
4817
+ }
4818
+ /**
4819
+ * Create a new task
4820
+ */
4821
+ async createTask(task) {
4822
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
4823
+ method: 'POST',
4824
+ body: JSON.stringify(task),
4825
+ });
4826
+ }
4827
+ /**
4828
+ * Update an existing task
4829
+ */
4830
+ async updateTask(taskId, updates) {
4831
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
4832
+ method: 'PUT',
4833
+ body: JSON.stringify(updates),
4834
+ });
4835
+ }
4836
+ /**
4837
+ * Mark a task as completed
4838
+ */
4839
+ async completeTask(taskId) {
4840
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}/complete`, {
4841
+ method: 'PATCH',
4842
+ });
4843
+ }
4844
+ /**
4845
+ * Delete a task
4846
+ */
4847
+ async deleteTask(taskId) {
4848
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
4849
+ method: 'DELETE',
4850
+ });
4851
+ }
4852
+ // ============================================
4853
+ // ACTIVITIES API
4854
+ // ============================================
4855
+ /**
4856
+ * Get activities for a contact
4857
+ */
4858
+ async getContactActivities(contactId, params) {
4859
+ const queryParams = new URLSearchParams();
4860
+ if (params?.page)
4861
+ queryParams.set('page', params.page.toString());
4862
+ if (params?.limit)
4863
+ queryParams.set('limit', params.limit.toString());
4864
+ if (params?.type)
4865
+ queryParams.set('type', params.type);
4866
+ const query = queryParams.toString();
4867
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
4868
+ return this.request(endpoint);
4869
+ }
4870
+ /**
4871
+ * Get activities for an opportunity/deal
4872
+ */
4873
+ async getOpportunityActivities(opportunityId, params) {
4874
+ const queryParams = new URLSearchParams();
4875
+ if (params?.page)
4876
+ queryParams.set('page', params.page.toString());
4877
+ if (params?.limit)
4878
+ queryParams.set('limit', params.limit.toString());
4879
+ if (params?.type)
4880
+ queryParams.set('type', params.type);
4881
+ const query = queryParams.toString();
4882
+ const endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/activities${query ? `?${query}` : ''}`;
4883
+ return this.request(endpoint);
4884
+ }
4885
+ /**
4886
+ * Create a new activity
4887
+ */
4888
+ async createActivity(activity) {
4889
+ // Determine the correct endpoint based on related entity
4890
+ let endpoint;
4891
+ if (activity.opportunityId) {
4892
+ endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${activity.opportunityId}/activities`;
4893
+ }
4894
+ else if (activity.contactId) {
4895
+ endpoint = `/api/workspaces/${this.workspaceId}/contacts/${activity.contactId}/activities`;
4896
+ }
4897
+ else {
4898
+ endpoint = `/api/workspaces/${this.workspaceId}/activities`;
4899
+ }
4900
+ return this.request(endpoint, {
4901
+ method: 'POST',
4902
+ body: JSON.stringify(activity),
4903
+ });
4904
+ }
4905
+ /**
4906
+ * Update an existing activity
4907
+ */
4908
+ async updateActivity(activityId, updates) {
4909
+ return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
4910
+ method: 'PATCH',
4911
+ body: JSON.stringify(updates),
4912
+ });
4913
+ }
4914
+ /**
4915
+ * Delete an activity
4916
+ */
4917
+ async deleteActivity(activityId) {
4918
+ return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
4919
+ method: 'DELETE',
4920
+ });
4921
+ }
4922
+ /**
4923
+ * Log a call activity
4924
+ */
4925
+ async logCall(data) {
4926
+ return this.createActivity({
4927
+ type: 'call',
4928
+ title: `${data.direction === 'inbound' ? 'Inbound' : 'Outbound'} Call`,
4929
+ direction: data.direction,
4930
+ duration: data.duration,
4931
+ outcome: data.outcome,
4932
+ description: data.notes,
4933
+ contactId: data.contactId,
4934
+ opportunityId: data.opportunityId,
4935
+ });
4936
+ }
4937
+ /**
4938
+ * Log a meeting activity
4939
+ */
4940
+ async logMeeting(data) {
4941
+ return this.createActivity({
4942
+ type: 'meeting',
4943
+ title: data.title,
4944
+ duration: data.duration,
4945
+ outcome: data.outcome,
4946
+ description: data.notes,
4947
+ contactId: data.contactId,
4948
+ opportunityId: data.opportunityId,
4949
+ });
4950
+ }
4951
+ /**
4952
+ * Add a note to a contact or opportunity
4953
+ */
4954
+ async addNote(data) {
4955
+ return this.createActivity({
4956
+ type: 'note',
4957
+ title: 'Note',
4958
+ description: data.content,
4959
+ contactId: data.contactId,
4960
+ opportunityId: data.opportunityId,
4961
+ });
4962
+ }
4963
+ // ============================================
4964
+ // EMAIL TEMPLATES API
4965
+ // ============================================
4966
+ /**
4967
+ * Get all email templates
4968
+ */
4969
+ async getEmailTemplates(params) {
4970
+ const queryParams = new URLSearchParams();
4971
+ if (params?.page)
4972
+ queryParams.set('page', params.page.toString());
4973
+ if (params?.limit)
4974
+ queryParams.set('limit', params.limit.toString());
4975
+ const query = queryParams.toString();
4976
+ const endpoint = `/api/workspaces/${this.workspaceId}/email-templates${query ? `?${query}` : ''}`;
4977
+ return this.request(endpoint);
4978
+ }
4979
+ /**
4980
+ * Get a single email template by ID
4981
+ */
4982
+ async getEmailTemplate(templateId) {
4983
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`);
4984
+ }
4985
+ /**
4986
+ * Create a new email template
4987
+ */
4988
+ async createEmailTemplate(template) {
4989
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates`, {
4990
+ method: 'POST',
4991
+ body: JSON.stringify(template),
4992
+ });
4993
+ }
4994
+ /**
4995
+ * Update an email template
4996
+ */
4997
+ async updateEmailTemplate(templateId, updates) {
4998
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
4999
+ method: 'PUT',
5000
+ body: JSON.stringify(updates),
5001
+ });
5002
+ }
5003
+ /**
5004
+ * Delete an email template
5005
+ */
5006
+ async deleteEmailTemplate(templateId) {
5007
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
5008
+ method: 'DELETE',
5009
+ });
5010
+ }
5011
+ /**
5012
+ * Send an email using a template
5013
+ */
5014
+ async sendEmail(data) {
5015
+ return this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
5016
+ method: 'POST',
5017
+ body: JSON.stringify(data),
5018
+ });
5019
+ }
5020
+ // ============================================
5021
+ // READ-BACK / DATA RETRIEVAL API
5022
+ // ============================================
5023
+ /**
5024
+ * Get a contact by email address.
5025
+ * Returns the first matching contact from a search query.
5026
+ */
5027
+ async getContactByEmail(email) {
5028
+ this.validateRequired('email', email, 'getContactByEmail');
5029
+ const queryParams = new URLSearchParams({ search: email, limit: '1' });
5030
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts?${queryParams.toString()}`);
5031
+ }
5032
+ /**
5033
+ * Get activity timeline for a contact
5034
+ */
5035
+ async getContactActivity(contactId, params) {
5036
+ this.validateRequired('contactId', contactId, 'getContactActivity');
5037
+ const queryParams = new URLSearchParams();
5038
+ if (params?.page)
5039
+ queryParams.set('page', params.page.toString());
5040
+ if (params?.limit)
5041
+ queryParams.set('limit', params.limit.toString());
5042
+ if (params?.type)
5043
+ queryParams.set('type', params.type);
5044
+ if (params?.startDate)
5045
+ queryParams.set('startDate', params.startDate);
5046
+ if (params?.endDate)
5047
+ queryParams.set('endDate', params.endDate);
5048
+ const query = queryParams.toString();
5049
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
5050
+ return this.request(endpoint);
5051
+ }
5052
+ /**
5053
+ * Get engagement metrics for a contact (via their linked visitor data)
5054
+ */
5055
+ async getContactEngagement(contactId) {
5056
+ this.validateRequired('contactId', contactId, 'getContactEngagement');
5057
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}/engagement`);
5058
+ }
5059
+ /**
5060
+ * Get a full timeline for a contact including events, activities, and opportunities
5061
+ */
5062
+ async getContactTimeline(contactId, params) {
5063
+ this.validateRequired('contactId', contactId, 'getContactTimeline');
5064
+ const queryParams = new URLSearchParams();
5065
+ if (params?.page)
5066
+ queryParams.set('page', params.page.toString());
5067
+ if (params?.limit)
5068
+ queryParams.set('limit', params.limit.toString());
5069
+ const query = queryParams.toString();
5070
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/timeline${query ? `?${query}` : ''}`;
5071
+ return this.request(endpoint);
5072
+ }
5073
+ /**
5074
+ * Search contacts with advanced filters
5075
+ */
5076
+ async searchContacts(query, filters) {
5077
+ const queryParams = new URLSearchParams();
5078
+ queryParams.set('search', query);
5079
+ if (filters?.status)
5080
+ queryParams.set('status', filters.status);
5081
+ if (filters?.lifecycleStage)
5082
+ queryParams.set('lifecycleStage', filters.lifecycleStage);
5083
+ if (filters?.source)
5084
+ queryParams.set('source', filters.source);
5085
+ if (filters?.tags)
5086
+ queryParams.set('tags', filters.tags.join(','));
5087
+ if (filters?.page)
5088
+ queryParams.set('page', filters.page.toString());
5089
+ if (filters?.limit)
5090
+ queryParams.set('limit', filters.limit.toString());
5091
+ const qs = queryParams.toString();
5092
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts${qs ? `?${qs}` : ''}`;
5093
+ return this.request(endpoint);
5094
+ }
5095
+ // ============================================
5096
+ // WEBHOOK MANAGEMENT API
5097
+ // ============================================
5098
+ /**
5099
+ * List all webhook subscriptions
5100
+ */
5101
+ async listWebhooks(params) {
5102
+ const queryParams = new URLSearchParams();
5103
+ if (params?.page)
5104
+ queryParams.set('page', params.page.toString());
5105
+ if (params?.limit)
5106
+ queryParams.set('limit', params.limit.toString());
5107
+ const query = queryParams.toString();
5108
+ return this.request(`/api/workspaces/${this.workspaceId}/webhooks${query ? `?${query}` : ''}`);
5109
+ }
5110
+ /**
5111
+ * Create a new webhook subscription
5112
+ */
5113
+ async createWebhook(data) {
5114
+ return this.request(`/api/workspaces/${this.workspaceId}/webhooks`, {
5115
+ method: 'POST',
5116
+ body: JSON.stringify(data),
5117
+ });
5118
+ }
5119
+ /**
5120
+ * Delete a webhook subscription
5121
+ */
5122
+ async deleteWebhook(webhookId) {
5123
+ this.validateRequired('webhookId', webhookId, 'deleteWebhook');
5124
+ return this.request(`/api/workspaces/${this.workspaceId}/webhooks/${webhookId}`, {
5125
+ method: 'DELETE',
5126
+ });
5127
+ }
5128
+ // ============================================
5129
+ // EVENT TRIGGERS API (delegated to triggers manager)
5130
+ // ============================================
5131
+ /**
5132
+ * Get all event triggers
5133
+ */
5134
+ async getEventTriggers() {
5135
+ return this.triggers.getTriggers();
5136
+ }
5137
+ /**
5138
+ * Create a new event trigger
5139
+ */
5140
+ async createEventTrigger(trigger) {
5141
+ return this.triggers.createTrigger(trigger);
5142
+ }
5143
+ /**
5144
+ * Update an event trigger
5145
+ */
5146
+ async updateEventTrigger(triggerId, updates) {
5147
+ return this.triggers.updateTrigger(triggerId, updates);
5148
+ }
5149
+ /**
5150
+ * Delete an event trigger
5151
+ */
5152
+ async deleteEventTrigger(triggerId) {
5153
+ return this.triggers.deleteTrigger(triggerId);
5154
+ }
5155
+ }
5156
+
3968
5157
  /**
3969
5158
  * Clianta SDK
3970
5159
  * Client-side tracking SDK for CRM — tracks visitors, identifies contacts,
@@ -4074,6 +5263,7 @@ if (typeof window !== 'undefined') {
4074
5263
  }
4075
5264
  }
4076
5265
 
5266
+ exports.CRMClient = CRMClient;
4077
5267
  exports.ConsentManager = ConsentManager;
4078
5268
  exports.SDK_VERSION = SDK_VERSION;
4079
5269
  exports.Tracker = Tracker;