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