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