@clianta/sdk 1.3.0 → 1.5.0

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.
package/dist/react.esm.js CHANGED
@@ -1,17 +1,17 @@
1
1
  /*!
2
- * Clianta SDK v1.3.0
2
+ * Clianta SDK v1.5.0
3
3
  * (c) 2026 Clianta
4
4
  * Released under the MIT License.
5
5
  */
6
6
  import { jsx } from 'react/jsx-runtime';
7
- import { createContext, useRef, useEffect, useContext } from 'react';
7
+ import { createContext, useState, useRef, useEffect, useContext } from 'react';
8
8
 
9
9
  /**
10
10
  * Clianta SDK - Configuration
11
11
  * @see SDK_VERSION in core/config.ts
12
12
  */
13
13
  /** SDK Version */
14
- const SDK_VERSION = '1.3.0';
14
+ const SDK_VERSION = '1.4.0';
15
15
  /** Default API endpoint based on environment */
16
16
  const getDefaultApiEndpoint = () => {
17
17
  if (typeof window === 'undefined')
@@ -37,6 +37,7 @@ const DEFAULT_CONFIG = {
37
37
  projectId: '',
38
38
  apiEndpoint: getDefaultApiEndpoint(),
39
39
  authToken: '',
40
+ apiKey: '',
40
41
  debug: false,
41
42
  autoPageView: true,
42
43
  plugins: DEFAULT_PLUGINS,
@@ -52,6 +53,7 @@ const DEFAULT_CONFIG = {
52
53
  cookieDomain: '',
53
54
  useCookies: false,
54
55
  cookielessMode: false,
56
+ persistMode: 'session',
55
57
  };
56
58
  /** Storage keys */
57
59
  const STORAGE_KEYS = {
@@ -184,12 +186,39 @@ class Transport {
184
186
  return this.send(url, payload);
185
187
  }
186
188
  /**
187
- * Send identify request
189
+ * Send identify request.
190
+ * Returns contactId from the server response so the Tracker can store it.
188
191
  */
189
192
  async sendIdentify(data) {
190
193
  const url = `${this.config.apiEndpoint}/api/public/track/identify`;
191
- const payload = JSON.stringify(data);
192
- return this.send(url, payload);
194
+ try {
195
+ const response = await this.fetchWithTimeout(url, {
196
+ method: 'POST',
197
+ headers: { 'Content-Type': 'application/json' },
198
+ body: JSON.stringify(data),
199
+ keepalive: true,
200
+ });
201
+ const body = await response.json().catch(() => ({}));
202
+ if (response.ok) {
203
+ logger.debug('Identify successful, contactId:', body.contactId);
204
+ return {
205
+ success: true,
206
+ status: response.status,
207
+ contactId: body.contactId ?? undefined,
208
+ };
209
+ }
210
+ if (response.status >= 500) {
211
+ logger.warn(`Identify server error (${response.status})`);
212
+ }
213
+ else {
214
+ logger.error(`Identify failed with status ${response.status}:`, body.message);
215
+ }
216
+ return { success: false, status: response.status };
217
+ }
218
+ catch (error) {
219
+ logger.error('Identify request failed:', error);
220
+ return { success: false, error: error };
221
+ }
193
222
  }
194
223
  /**
195
224
  * Send events synchronously (for page unload)
@@ -218,6 +247,39 @@ class Transport {
218
247
  return false;
219
248
  }
220
249
  }
250
+ /**
251
+ * Fetch data from the tracking API (GET request)
252
+ * Used for read-back APIs (visitor profile, activity, etc.)
253
+ */
254
+ async fetchData(path, params) {
255
+ const url = new URL(`${this.config.apiEndpoint}${path}`);
256
+ if (params) {
257
+ Object.entries(params).forEach(([key, value]) => {
258
+ if (value !== undefined && value !== null) {
259
+ url.searchParams.set(key, value);
260
+ }
261
+ });
262
+ }
263
+ try {
264
+ const response = await this.fetchWithTimeout(url.toString(), {
265
+ method: 'GET',
266
+ headers: {
267
+ 'Accept': 'application/json',
268
+ },
269
+ });
270
+ if (response.ok) {
271
+ const body = await response.json();
272
+ logger.debug('Fetch successful:', path);
273
+ return { success: true, data: body.data ?? body, status: response.status };
274
+ }
275
+ logger.error(`Fetch failed with status ${response.status}`);
276
+ return { success: false, status: response.status };
277
+ }
278
+ catch (error) {
279
+ logger.error('Fetch request failed:', error);
280
+ return { success: false, error: error };
281
+ }
282
+ }
221
283
  /**
222
284
  * Internal send with retry logic
223
285
  */
@@ -382,7 +444,9 @@ function cookie(name, value, days) {
382
444
  date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
383
445
  expires = '; expires=' + date.toUTCString();
384
446
  }
385
- document.cookie = name + '=' + value + expires + '; path=/; SameSite=Lax';
447
+ // Add Secure flag on HTTPS to prevent cookie leakage over plaintext
448
+ const secure = typeof location !== 'undefined' && location.protocol === 'https:' ? '; Secure' : '';
449
+ document.cookie = name + '=' + value + expires + '; path=/; SameSite=Lax' + secure;
386
450
  return value;
387
451
  }
388
452
  // ============================================
@@ -546,6 +610,17 @@ function isMobile() {
546
610
  return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
547
611
  }
548
612
  // ============================================
613
+ // VALIDATION UTILITIES
614
+ // ============================================
615
+ /**
616
+ * Validate email format
617
+ */
618
+ function isValidEmail(email) {
619
+ if (typeof email !== 'string' || !email)
620
+ return false;
621
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
622
+ }
623
+ // ============================================
549
624
  // DEVICE INFO
550
625
  // ============================================
551
626
  /**
@@ -599,6 +674,7 @@ class EventQueue {
599
674
  maxQueueSize: config.maxQueueSize ?? MAX_QUEUE_SIZE,
600
675
  storageKey: config.storageKey ?? STORAGE_KEYS.EVENT_QUEUE,
601
676
  };
677
+ this.persistMode = config.persistMode || 'session';
602
678
  // Restore persisted queue
603
679
  this.restoreQueue();
604
680
  // Start auto-flush timer
@@ -704,6 +780,13 @@ class EventQueue {
704
780
  clear() {
705
781
  this.queue = [];
706
782
  this.persistQueue([]);
783
+ // Also clear localStorage if used
784
+ if (this.persistMode === 'local' && typeof localStorage !== 'undefined') {
785
+ try {
786
+ localStorage.removeItem(this.config.storageKey);
787
+ }
788
+ catch { /* ignore */ }
789
+ }
707
790
  }
708
791
  /**
709
792
  * Stop the flush timer and cleanup handlers
@@ -758,22 +841,44 @@ class EventQueue {
758
841
  window.addEventListener('pagehide', this.boundPageHide);
759
842
  }
760
843
  /**
761
- * Persist queue to localStorage
844
+ * Persist queue to storage based on persistMode
762
845
  */
763
846
  persistQueue(events) {
847
+ if (this.persistMode === 'none')
848
+ return;
764
849
  try {
765
- setLocalStorage(this.config.storageKey, JSON.stringify(events));
850
+ const serialized = JSON.stringify(events);
851
+ if (this.persistMode === 'local' && typeof localStorage !== 'undefined') {
852
+ try {
853
+ localStorage.setItem(this.config.storageKey, serialized);
854
+ }
855
+ catch {
856
+ // localStorage quota exceeded — fallback to sessionStorage
857
+ setSessionStorage(this.config.storageKey, serialized);
858
+ }
859
+ }
860
+ else {
861
+ setSessionStorage(this.config.storageKey, serialized);
862
+ }
766
863
  }
767
864
  catch {
768
865
  // Ignore storage errors
769
866
  }
770
867
  }
771
868
  /**
772
- * Restore queue from localStorage
869
+ * Restore queue from storage
773
870
  */
774
871
  restoreQueue() {
775
872
  try {
776
- const stored = getLocalStorage(this.config.storageKey);
873
+ let stored = null;
874
+ // Check localStorage first (cross-session persistence)
875
+ if (this.persistMode === 'local' && typeof localStorage !== 'undefined') {
876
+ stored = localStorage.getItem(this.config.storageKey);
877
+ }
878
+ // Fall back to sessionStorage
879
+ if (!stored) {
880
+ stored = getSessionStorage(this.config.storageKey);
881
+ }
777
882
  if (stored) {
778
883
  const events = JSON.parse(stored);
779
884
  if (Array.isArray(events) && events.length > 0) {
@@ -841,10 +946,13 @@ class PageViewPlugin extends BasePlugin {
841
946
  history.pushState = function (...args) {
842
947
  self.originalPushState.apply(history, args);
843
948
  self.trackPageView();
949
+ // Notify other plugins (e.g. ScrollPlugin) about navigation
950
+ window.dispatchEvent(new Event('clianta:navigation'));
844
951
  };
845
952
  history.replaceState = function (...args) {
846
953
  self.originalReplaceState.apply(history, args);
847
954
  self.trackPageView();
955
+ window.dispatchEvent(new Event('clianta:navigation'));
848
956
  };
849
957
  // Handle back/forward navigation
850
958
  this.popstateHandler = () => this.trackPageView();
@@ -898,9 +1006,8 @@ class ScrollPlugin extends BasePlugin {
898
1006
  this.pageLoadTime = 0;
899
1007
  this.scrollTimeout = null;
900
1008
  this.boundHandler = null;
901
- /** SPA navigation support */
902
- this.originalPushState = null;
903
- this.originalReplaceState = null;
1009
+ /** SPA navigation listen for PageViewPlugin's custom event instead of patching history */
1010
+ this.navigationHandler = null;
904
1011
  this.popstateHandler = null;
905
1012
  }
906
1013
  init(tracker) {
@@ -909,8 +1016,13 @@ class ScrollPlugin extends BasePlugin {
909
1016
  if (typeof window !== 'undefined') {
910
1017
  this.boundHandler = this.handleScroll.bind(this);
911
1018
  window.addEventListener('scroll', this.boundHandler, { passive: true });
912
- // Setup SPA navigation reset
913
- this.setupNavigationReset();
1019
+ // Listen for navigation events dispatched by PageViewPlugin
1020
+ // instead of independently monkey-patching history.pushState
1021
+ this.navigationHandler = () => this.resetForNavigation();
1022
+ window.addEventListener('clianta:navigation', this.navigationHandler);
1023
+ // Handle back/forward navigation
1024
+ this.popstateHandler = () => this.resetForNavigation();
1025
+ window.addEventListener('popstate', this.popstateHandler);
914
1026
  }
915
1027
  }
916
1028
  destroy() {
@@ -920,16 +1032,10 @@ class ScrollPlugin extends BasePlugin {
920
1032
  if (this.scrollTimeout) {
921
1033
  clearTimeout(this.scrollTimeout);
922
1034
  }
923
- // Restore original history methods
924
- if (this.originalPushState) {
925
- history.pushState = this.originalPushState;
926
- this.originalPushState = null;
927
- }
928
- if (this.originalReplaceState) {
929
- history.replaceState = this.originalReplaceState;
930
- this.originalReplaceState = null;
1035
+ if (this.navigationHandler && typeof window !== 'undefined') {
1036
+ window.removeEventListener('clianta:navigation', this.navigationHandler);
1037
+ this.navigationHandler = null;
931
1038
  }
932
- // Remove popstate listener
933
1039
  if (this.popstateHandler && typeof window !== 'undefined') {
934
1040
  window.removeEventListener('popstate', this.popstateHandler);
935
1041
  this.popstateHandler = null;
@@ -944,29 +1050,6 @@ class ScrollPlugin extends BasePlugin {
944
1050
  this.maxScrollDepth = 0;
945
1051
  this.pageLoadTime = Date.now();
946
1052
  }
947
- /**
948
- * Setup History API interception for SPA navigation
949
- */
950
- setupNavigationReset() {
951
- if (typeof window === 'undefined')
952
- return;
953
- // Store originals for cleanup
954
- this.originalPushState = history.pushState;
955
- this.originalReplaceState = history.replaceState;
956
- // Intercept pushState and replaceState
957
- const self = this;
958
- history.pushState = function (...args) {
959
- self.originalPushState.apply(history, args);
960
- self.resetForNavigation();
961
- };
962
- history.replaceState = function (...args) {
963
- self.originalReplaceState.apply(history, args);
964
- self.resetForNavigation();
965
- };
966
- // Handle back/forward navigation
967
- this.popstateHandler = () => this.resetForNavigation();
968
- window.addEventListener('popstate', this.popstateHandler);
969
- }
970
1053
  handleScroll() {
971
1054
  // Debounce scroll tracking
972
1055
  if (this.scrollTimeout) {
@@ -1166,6 +1249,10 @@ class ClicksPlugin extends BasePlugin {
1166
1249
  elementId: elementInfo.id,
1167
1250
  elementClass: elementInfo.className,
1168
1251
  href: target.href || undefined,
1252
+ x: Math.round((e.clientX / window.innerWidth) * 100),
1253
+ y: Math.round((e.clientY / window.innerHeight) * 100),
1254
+ viewportWidth: window.innerWidth,
1255
+ viewportHeight: window.innerHeight,
1169
1256
  });
1170
1257
  }
1171
1258
  }
@@ -1188,6 +1275,9 @@ class EngagementPlugin extends BasePlugin {
1188
1275
  this.boundMarkEngaged = null;
1189
1276
  this.boundTrackTimeOnPage = null;
1190
1277
  this.boundVisibilityHandler = null;
1278
+ /** SPA navigation — listen for PageViewPlugin's custom event instead of patching history */
1279
+ this.navigationHandler = null;
1280
+ this.popstateHandler = null;
1191
1281
  }
1192
1282
  init(tracker) {
1193
1283
  super.init(tracker);
@@ -1213,6 +1303,13 @@ class EngagementPlugin extends BasePlugin {
1213
1303
  // Track time on page before unload
1214
1304
  window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
1215
1305
  document.addEventListener('visibilitychange', this.boundVisibilityHandler);
1306
+ // Listen for navigation events dispatched by PageViewPlugin
1307
+ // instead of independently monkey-patching history.pushState
1308
+ this.navigationHandler = () => this.resetForNavigation();
1309
+ window.addEventListener('clianta:navigation', this.navigationHandler);
1310
+ // Handle back/forward navigation
1311
+ this.popstateHandler = () => this.resetForNavigation();
1312
+ window.addEventListener('popstate', this.popstateHandler);
1216
1313
  }
1217
1314
  destroy() {
1218
1315
  if (this.boundMarkEngaged && typeof document !== 'undefined') {
@@ -1226,11 +1323,28 @@ class EngagementPlugin extends BasePlugin {
1226
1323
  if (this.boundVisibilityHandler && typeof document !== 'undefined') {
1227
1324
  document.removeEventListener('visibilitychange', this.boundVisibilityHandler);
1228
1325
  }
1326
+ if (this.navigationHandler && typeof window !== 'undefined') {
1327
+ window.removeEventListener('clianta:navigation', this.navigationHandler);
1328
+ this.navigationHandler = null;
1329
+ }
1330
+ if (this.popstateHandler && typeof window !== 'undefined') {
1331
+ window.removeEventListener('popstate', this.popstateHandler);
1332
+ this.popstateHandler = null;
1333
+ }
1229
1334
  if (this.engagementTimeout) {
1230
1335
  clearTimeout(this.engagementTimeout);
1231
1336
  }
1232
1337
  super.destroy();
1233
1338
  }
1339
+ resetForNavigation() {
1340
+ this.pageLoadTime = Date.now();
1341
+ this.engagementStartTime = Date.now();
1342
+ this.isEngaged = false;
1343
+ if (this.engagementTimeout) {
1344
+ clearTimeout(this.engagementTimeout);
1345
+ this.engagementTimeout = null;
1346
+ }
1347
+ }
1234
1348
  markEngaged() {
1235
1349
  if (!this.isEngaged) {
1236
1350
  this.isEngaged = true;
@@ -1270,9 +1384,8 @@ class DownloadsPlugin extends BasePlugin {
1270
1384
  this.name = 'downloads';
1271
1385
  this.trackedDownloads = new Set();
1272
1386
  this.boundHandler = null;
1273
- /** SPA navigation support */
1274
- this.originalPushState = null;
1275
- this.originalReplaceState = null;
1387
+ /** SPA navigation listen for PageViewPlugin's custom event instead of patching history */
1388
+ this.navigationHandler = null;
1276
1389
  this.popstateHandler = null;
1277
1390
  }
1278
1391
  init(tracker) {
@@ -1280,24 +1393,25 @@ class DownloadsPlugin extends BasePlugin {
1280
1393
  if (typeof document !== 'undefined') {
1281
1394
  this.boundHandler = this.handleClick.bind(this);
1282
1395
  document.addEventListener('click', this.boundHandler, true);
1283
- // Setup SPA navigation reset
1284
- this.setupNavigationReset();
1396
+ }
1397
+ if (typeof window !== 'undefined') {
1398
+ // Listen for navigation events dispatched by PageViewPlugin
1399
+ // instead of independently monkey-patching history.pushState
1400
+ this.navigationHandler = () => this.resetForNavigation();
1401
+ window.addEventListener('clianta:navigation', this.navigationHandler);
1402
+ // Handle back/forward navigation
1403
+ this.popstateHandler = () => this.resetForNavigation();
1404
+ window.addEventListener('popstate', this.popstateHandler);
1285
1405
  }
1286
1406
  }
1287
1407
  destroy() {
1288
1408
  if (this.boundHandler && typeof document !== 'undefined') {
1289
1409
  document.removeEventListener('click', this.boundHandler, true);
1290
1410
  }
1291
- // Restore original history methods
1292
- if (this.originalPushState) {
1293
- history.pushState = this.originalPushState;
1294
- this.originalPushState = null;
1295
- }
1296
- if (this.originalReplaceState) {
1297
- history.replaceState = this.originalReplaceState;
1298
- this.originalReplaceState = null;
1411
+ if (this.navigationHandler && typeof window !== 'undefined') {
1412
+ window.removeEventListener('clianta:navigation', this.navigationHandler);
1413
+ this.navigationHandler = null;
1299
1414
  }
1300
- // Remove popstate listener
1301
1415
  if (this.popstateHandler && typeof window !== 'undefined') {
1302
1416
  window.removeEventListener('popstate', this.popstateHandler);
1303
1417
  this.popstateHandler = null;
@@ -1310,29 +1424,6 @@ class DownloadsPlugin extends BasePlugin {
1310
1424
  resetForNavigation() {
1311
1425
  this.trackedDownloads.clear();
1312
1426
  }
1313
- /**
1314
- * Setup History API interception for SPA navigation
1315
- */
1316
- setupNavigationReset() {
1317
- if (typeof window === 'undefined')
1318
- return;
1319
- // Store originals for cleanup
1320
- this.originalPushState = history.pushState;
1321
- this.originalReplaceState = history.replaceState;
1322
- // Intercept pushState and replaceState
1323
- const self = this;
1324
- history.pushState = function (...args) {
1325
- self.originalPushState.apply(history, args);
1326
- self.resetForNavigation();
1327
- };
1328
- history.replaceState = function (...args) {
1329
- self.originalReplaceState.apply(history, args);
1330
- self.resetForNavigation();
1331
- };
1332
- // Handle back/forward navigation
1333
- this.popstateHandler = () => this.resetForNavigation();
1334
- window.addEventListener('popstate', this.popstateHandler);
1335
- }
1336
1427
  handleClick(e) {
1337
1428
  const link = e.target.closest('a');
1338
1429
  if (!link || !link.href)
@@ -1368,6 +1459,9 @@ class ExitIntentPlugin extends BasePlugin {
1368
1459
  this.exitIntentShown = false;
1369
1460
  this.pageLoadTime = 0;
1370
1461
  this.boundHandler = null;
1462
+ /** SPA navigation — listen for PageViewPlugin's custom event instead of patching history */
1463
+ this.navigationHandler = null;
1464
+ this.popstateHandler = null;
1371
1465
  }
1372
1466
  init(tracker) {
1373
1467
  super.init(tracker);
@@ -1379,13 +1473,34 @@ class ExitIntentPlugin extends BasePlugin {
1379
1473
  this.boundHandler = this.handleMouseLeave.bind(this);
1380
1474
  document.addEventListener('mouseleave', this.boundHandler);
1381
1475
  }
1476
+ if (typeof window !== 'undefined') {
1477
+ // Listen for navigation events dispatched by PageViewPlugin
1478
+ // instead of independently monkey-patching history.pushState
1479
+ this.navigationHandler = () => this.resetForNavigation();
1480
+ window.addEventListener('clianta:navigation', this.navigationHandler);
1481
+ // Handle back/forward navigation
1482
+ this.popstateHandler = () => this.resetForNavigation();
1483
+ window.addEventListener('popstate', this.popstateHandler);
1484
+ }
1382
1485
  }
1383
1486
  destroy() {
1384
1487
  if (this.boundHandler && typeof document !== 'undefined') {
1385
1488
  document.removeEventListener('mouseleave', this.boundHandler);
1386
1489
  }
1490
+ if (this.navigationHandler && typeof window !== 'undefined') {
1491
+ window.removeEventListener('clianta:navigation', this.navigationHandler);
1492
+ this.navigationHandler = null;
1493
+ }
1494
+ if (this.popstateHandler && typeof window !== 'undefined') {
1495
+ window.removeEventListener('popstate', this.popstateHandler);
1496
+ this.popstateHandler = null;
1497
+ }
1387
1498
  super.destroy();
1388
1499
  }
1500
+ resetForNavigation() {
1501
+ this.exitIntentShown = false;
1502
+ this.pageLoadTime = Date.now();
1503
+ }
1389
1504
  handleMouseLeave(e) {
1390
1505
  // Only trigger when mouse leaves from the top of the page
1391
1506
  if (e.clientY > 0 || this.exitIntentShown)
@@ -1613,6 +1728,8 @@ class PopupFormsPlugin extends BasePlugin {
1613
1728
  this.shownForms = new Set();
1614
1729
  this.scrollHandler = null;
1615
1730
  this.exitHandler = null;
1731
+ this.delayTimers = [];
1732
+ this.clickTriggerListeners = [];
1616
1733
  }
1617
1734
  async init(tracker) {
1618
1735
  super.init(tracker);
@@ -1627,6 +1744,14 @@ class PopupFormsPlugin extends BasePlugin {
1627
1744
  }
1628
1745
  destroy() {
1629
1746
  this.removeTriggers();
1747
+ for (const timer of this.delayTimers) {
1748
+ clearTimeout(timer);
1749
+ }
1750
+ this.delayTimers = [];
1751
+ for (const { element, handler } of this.clickTriggerListeners) {
1752
+ element.removeEventListener('click', handler);
1753
+ }
1754
+ this.clickTriggerListeners = [];
1630
1755
  super.destroy();
1631
1756
  }
1632
1757
  loadShownForms() {
@@ -1689,7 +1814,7 @@ class PopupFormsPlugin extends BasePlugin {
1689
1814
  this.forms.forEach(form => {
1690
1815
  switch (form.trigger.type) {
1691
1816
  case 'delay':
1692
- setTimeout(() => this.showForm(form), (form.trigger.value || 5) * 1000);
1817
+ this.delayTimers.push(setTimeout(() => this.showForm(form), (form.trigger.value || 5) * 1000));
1693
1818
  break;
1694
1819
  case 'scroll':
1695
1820
  this.setupScrollTrigger(form);
@@ -1732,7 +1857,9 @@ class PopupFormsPlugin extends BasePlugin {
1732
1857
  return;
1733
1858
  const elements = document.querySelectorAll(form.trigger.selector);
1734
1859
  elements.forEach(el => {
1735
- el.addEventListener('click', () => this.showForm(form));
1860
+ const handler = () => this.showForm(form);
1861
+ el.addEventListener('click', handler);
1862
+ this.clickTriggerListeners.push({ element: el, handler });
1736
1863
  });
1737
1864
  }
1738
1865
  removeTriggers() {
@@ -2019,7 +2146,7 @@ class PopupFormsPlugin extends BasePlugin {
2019
2146
  const submitBtn = formElement.querySelector('button[type="submit"]');
2020
2147
  if (submitBtn) {
2021
2148
  submitBtn.disabled = true;
2022
- submitBtn.innerHTML = 'Submitting...';
2149
+ submitBtn.textContent = 'Submitting...';
2023
2150
  }
2024
2151
  try {
2025
2152
  const response = await fetch(`${apiEndpoint}/api/public/lead-forms/${form._id}/submit`, {
@@ -2060,11 +2187,24 @@ class PopupFormsPlugin extends BasePlugin {
2060
2187
  if (data.email) {
2061
2188
  this.tracker?.identify(data.email, data);
2062
2189
  }
2063
- // Redirect if configured
2190
+ // Redirect if configured (validate URL to prevent open redirect)
2064
2191
  if (form.redirectUrl) {
2065
- setTimeout(() => {
2066
- window.location.href = form.redirectUrl;
2067
- }, 1500);
2192
+ try {
2193
+ const redirect = new URL(form.redirectUrl, window.location.origin);
2194
+ const isSameOrigin = redirect.origin === window.location.origin;
2195
+ const isSafeProtocol = redirect.protocol === 'https:' || redirect.protocol === 'http:';
2196
+ if (isSameOrigin || isSafeProtocol) {
2197
+ setTimeout(() => {
2198
+ window.location.href = redirect.href;
2199
+ }, 1500);
2200
+ }
2201
+ else {
2202
+ console.warn('[Clianta] Blocked unsafe redirect URL:', form.redirectUrl);
2203
+ }
2204
+ }
2205
+ catch {
2206
+ console.warn('[Clianta] Invalid redirect URL:', form.redirectUrl);
2207
+ }
2068
2208
  }
2069
2209
  // Close after delay
2070
2210
  setTimeout(() => {
@@ -2079,7 +2219,7 @@ class PopupFormsPlugin extends BasePlugin {
2079
2219
  console.error('[Clianta] Form submit error:', error);
2080
2220
  if (submitBtn) {
2081
2221
  submitBtn.disabled = false;
2082
- submitBtn.innerHTML = form.submitButtonText || 'Subscribe';
2222
+ submitBtn.textContent = form.submitButtonText || 'Subscribe';
2083
2223
  }
2084
2224
  }
2085
2225
  }
@@ -2364,355 +2504,478 @@ class ConsentManager {
2364
2504
  }
2365
2505
 
2366
2506
  /**
2367
- * Clianta SDK - Main Tracker Class
2368
- * @see SDK_VERSION in core/config.ts
2507
+ * Clianta SDK - Event Triggers Manager
2508
+ * Manages event-driven automation and email notifications
2369
2509
  */
2370
2510
  /**
2371
- * Main Clianta Tracker Class
2511
+ * Event Triggers Manager
2512
+ * Handles event-driven automation based on CRM actions
2513
+ *
2514
+ * Similar to:
2515
+ * - Salesforce: Process Builder, Flow Automation
2516
+ * - HubSpot: Workflows, Email Sequences
2517
+ * - Pipedrive: Workflow Automation
2372
2518
  */
2373
- class Tracker {
2374
- constructor(workspaceId, userConfig = {}) {
2375
- this.plugins = [];
2376
- this.isInitialized = false;
2377
- /** Pending identify retry on next flush */
2378
- this.pendingIdentify = null;
2379
- if (!workspaceId) {
2380
- throw new Error('[Clianta] Workspace ID is required');
2381
- }
2519
+ class EventTriggersManager {
2520
+ constructor(apiEndpoint, workspaceId, authToken) {
2521
+ this.triggers = new Map();
2522
+ this.listeners = new Map();
2523
+ this.apiEndpoint = apiEndpoint;
2382
2524
  this.workspaceId = workspaceId;
2383
- this.config = mergeConfig(userConfig);
2384
- // Setup debug mode
2385
- logger.enabled = this.config.debug;
2386
- logger.info(`Initializing SDK v${SDK_VERSION}`, { workspaceId });
2387
- // Initialize consent manager
2388
- this.consentManager = new ConsentManager({
2389
- ...this.config.consent,
2390
- onConsentChange: (state, previous) => {
2391
- this.onConsentChange(state, previous);
2392
- },
2393
- });
2394
- // Initialize transport and queue
2395
- this.transport = new Transport({ apiEndpoint: this.config.apiEndpoint });
2396
- this.queue = new EventQueue(this.transport, {
2397
- batchSize: this.config.batchSize,
2398
- flushInterval: this.config.flushInterval,
2399
- });
2400
- // Get or create visitor and session IDs based on mode
2401
- this.visitorId = this.createVisitorId();
2402
- this.sessionId = this.createSessionId();
2403
- logger.debug('IDs created', { visitorId: this.visitorId, sessionId: this.sessionId });
2404
- // Initialize plugins
2405
- this.initPlugins();
2406
- this.isInitialized = true;
2407
- logger.info('SDK initialized successfully');
2525
+ this.authToken = authToken;
2408
2526
  }
2409
2527
  /**
2410
- * Create visitor ID based on storage mode
2528
+ * Set authentication token
2411
2529
  */
2412
- createVisitorId() {
2413
- // Anonymous mode: use temporary ID until consent
2414
- if (this.config.consent.anonymousMode && !this.consentManager.hasExplicit()) {
2415
- const key = STORAGE_KEYS.VISITOR_ID + '_anon';
2416
- let anonId = getSessionStorage(key);
2417
- if (!anonId) {
2418
- anonId = 'anon_' + generateUUID();
2419
- setSessionStorage(key, anonId);
2420
- }
2421
- return anonId;
2530
+ setAuthToken(token) {
2531
+ this.authToken = token;
2532
+ }
2533
+ /**
2534
+ * Make authenticated API request
2535
+ */
2536
+ async request(endpoint, options = {}) {
2537
+ const url = `${this.apiEndpoint}${endpoint}`;
2538
+ const headers = {
2539
+ 'Content-Type': 'application/json',
2540
+ ...(options.headers || {}),
2541
+ };
2542
+ if (this.authToken) {
2543
+ headers['Authorization'] = `Bearer ${this.authToken}`;
2422
2544
  }
2423
- // Cookie-less mode: use sessionStorage only
2424
- if (this.config.cookielessMode) {
2425
- let visitorId = getSessionStorage(STORAGE_KEYS.VISITOR_ID);
2426
- if (!visitorId) {
2427
- visitorId = generateUUID();
2428
- setSessionStorage(STORAGE_KEYS.VISITOR_ID, visitorId);
2545
+ try {
2546
+ const response = await fetch(url, {
2547
+ ...options,
2548
+ headers,
2549
+ });
2550
+ const data = await response.json();
2551
+ if (!response.ok) {
2552
+ return {
2553
+ success: false,
2554
+ error: data.message || 'Request failed',
2555
+ status: response.status,
2556
+ };
2429
2557
  }
2430
- return visitorId;
2558
+ return {
2559
+ success: true,
2560
+ data: data.data || data,
2561
+ status: response.status,
2562
+ };
2563
+ }
2564
+ catch (error) {
2565
+ return {
2566
+ success: false,
2567
+ error: error instanceof Error ? error.message : 'Network error',
2568
+ status: 0,
2569
+ };
2431
2570
  }
2432
- // Normal mode
2433
- return getOrCreateVisitorId(this.config.useCookies);
2434
2571
  }
2572
+ // ============================================
2573
+ // TRIGGER MANAGEMENT
2574
+ // ============================================
2435
2575
  /**
2436
- * Create session ID
2576
+ * Get all event triggers
2437
2577
  */
2438
- createSessionId() {
2439
- return getOrCreateSessionId(this.config.sessionTimeout);
2440
- }
2441
- /**
2442
- * Handle consent state changes
2443
- */
2444
- onConsentChange(state, previous) {
2445
- logger.debug('Consent changed:', { from: previous, to: state });
2446
- // If analytics consent was just granted
2447
- if (state.analytics && !previous.analytics) {
2448
- // Upgrade from anonymous ID to persistent ID
2449
- if (this.config.consent.anonymousMode) {
2450
- this.visitorId = getOrCreateVisitorId(this.config.useCookies);
2451
- logger.info('Upgraded from anonymous to persistent visitor ID');
2452
- }
2453
- // Flush buffered events
2454
- const buffered = this.consentManager.flushBuffer();
2455
- for (const event of buffered) {
2456
- // Update event with new visitor ID
2457
- event.visitorId = this.visitorId;
2458
- this.queue.push(event);
2459
- }
2460
- }
2578
+ async getTriggers() {
2579
+ return this.request(`/api/workspaces/${this.workspaceId}/triggers`);
2461
2580
  }
2462
2581
  /**
2463
- * Initialize enabled plugins
2464
- * Handles both sync and async plugin init methods
2582
+ * Get a single trigger by ID
2465
2583
  */
2466
- initPlugins() {
2467
- const pluginsToLoad = this.config.plugins;
2468
- // Skip pageView plugin if autoPageView is disabled
2469
- const filteredPlugins = this.config.autoPageView
2470
- ? pluginsToLoad
2471
- : pluginsToLoad.filter((p) => p !== 'pageView');
2472
- for (const pluginName of filteredPlugins) {
2473
- try {
2474
- const plugin = getPlugin(pluginName);
2475
- // Handle both sync and async init (fire-and-forget for async)
2476
- const result = plugin.init(this);
2477
- if (result instanceof Promise) {
2478
- result.catch((error) => {
2479
- logger.error(`Async plugin init failed: ${pluginName}`, error);
2480
- });
2481
- }
2482
- this.plugins.push(plugin);
2483
- logger.debug(`Plugin loaded: ${pluginName}`);
2484
- }
2485
- catch (error) {
2486
- logger.error(`Failed to load plugin: ${pluginName}`, error);
2487
- }
2488
- }
2584
+ async getTrigger(triggerId) {
2585
+ return this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`);
2489
2586
  }
2490
2587
  /**
2491
- * Track a custom event
2588
+ * Create a new event trigger
2492
2589
  */
2493
- track(eventType, eventName, properties = {}) {
2494
- if (!this.isInitialized) {
2495
- logger.warn('SDK not initialized, event dropped');
2496
- return;
2497
- }
2498
- const event = {
2499
- workspaceId: this.workspaceId,
2500
- visitorId: this.visitorId,
2501
- sessionId: this.sessionId,
2502
- eventType: eventType,
2503
- eventName,
2504
- url: typeof window !== 'undefined' ? window.location.href : '',
2505
- referrer: typeof document !== 'undefined' ? document.referrer || undefined : undefined,
2506
- properties,
2507
- device: getDeviceInfo(),
2508
- ...getUTMParams(),
2509
- timestamp: new Date().toISOString(),
2510
- sdkVersion: SDK_VERSION,
2511
- };
2512
- // Check consent before tracking
2513
- if (!this.consentManager.canTrack()) {
2514
- // Buffer event for later if waitForConsent is enabled
2515
- if (this.config.consent.waitForConsent) {
2516
- this.consentManager.bufferEvent(event);
2517
- return;
2518
- }
2519
- // Otherwise drop the event
2520
- logger.debug('Event dropped (no consent):', eventName);
2521
- return;
2590
+ async createTrigger(trigger) {
2591
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers`, {
2592
+ method: 'POST',
2593
+ body: JSON.stringify(trigger),
2594
+ });
2595
+ // Cache the trigger locally if successful
2596
+ if (result.success && result.data?._id) {
2597
+ this.triggers.set(result.data._id, result.data);
2522
2598
  }
2523
- this.queue.push(event);
2524
- logger.debug('Event tracked:', eventName, properties);
2599
+ return result;
2525
2600
  }
2526
2601
  /**
2527
- * Track a page view
2602
+ * Update an existing trigger
2528
2603
  */
2529
- page(name, properties = {}) {
2530
- const pageName = name || (typeof document !== 'undefined' ? document.title : 'Page View');
2531
- this.track('page_view', pageName, {
2532
- ...properties,
2533
- path: typeof window !== 'undefined' ? window.location.pathname : '',
2604
+ async updateTrigger(triggerId, updates) {
2605
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
2606
+ method: 'PUT',
2607
+ body: JSON.stringify(updates),
2534
2608
  });
2609
+ // Update cache if successful
2610
+ if (result.success && result.data?._id) {
2611
+ this.triggers.set(result.data._id, result.data);
2612
+ }
2613
+ return result;
2535
2614
  }
2536
2615
  /**
2537
- * Identify a visitor
2616
+ * Delete a trigger
2538
2617
  */
2539
- async identify(email, traits = {}) {
2540
- if (!email) {
2541
- logger.warn('Email is required for identification');
2542
- return;
2543
- }
2544
- logger.info('Identifying visitor:', email);
2545
- const result = await this.transport.sendIdentify({
2546
- workspaceId: this.workspaceId,
2547
- visitorId: this.visitorId,
2548
- email,
2549
- properties: traits,
2618
+ async deleteTrigger(triggerId) {
2619
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
2620
+ method: 'DELETE',
2550
2621
  });
2622
+ // Remove from cache if successful
2551
2623
  if (result.success) {
2552
- logger.info('Visitor identified successfully');
2553
- this.pendingIdentify = null;
2554
- }
2555
- else {
2556
- logger.error('Failed to identify visitor:', result.error);
2557
- // Store for retry on next flush
2558
- this.pendingIdentify = { email, traits };
2624
+ this.triggers.delete(triggerId);
2559
2625
  }
2626
+ return result;
2560
2627
  }
2561
2628
  /**
2562
- * Retry pending identify call
2563
- */
2564
- async retryPendingIdentify() {
2565
- if (!this.pendingIdentify)
2566
- return;
2567
- const { email, traits } = this.pendingIdentify;
2568
- this.pendingIdentify = null;
2569
- await this.identify(email, traits);
2570
- }
2571
- /**
2572
- * Update consent state
2573
- */
2574
- consent(state) {
2575
- this.consentManager.update(state);
2576
- }
2577
- /**
2578
- * Get current consent state
2629
+ * Activate a trigger
2579
2630
  */
2580
- getConsentState() {
2581
- return this.consentManager.getState();
2631
+ async activateTrigger(triggerId) {
2632
+ return this.updateTrigger(triggerId, { isActive: true });
2582
2633
  }
2583
2634
  /**
2584
- * Toggle debug mode
2635
+ * Deactivate a trigger
2585
2636
  */
2586
- debug(enabled) {
2587
- logger.enabled = enabled;
2588
- logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
2637
+ async deactivateTrigger(triggerId) {
2638
+ return this.updateTrigger(triggerId, { isActive: false });
2589
2639
  }
2640
+ // ============================================
2641
+ // EVENT HANDLING (CLIENT-SIDE)
2642
+ // ============================================
2590
2643
  /**
2591
- * Get visitor ID
2644
+ * Register a local event listener for client-side triggers
2645
+ * This allows immediate client-side reactions to events
2592
2646
  */
2593
- getVisitorId() {
2594
- return this.visitorId;
2647
+ on(eventType, callback) {
2648
+ if (!this.listeners.has(eventType)) {
2649
+ this.listeners.set(eventType, new Set());
2650
+ }
2651
+ this.listeners.get(eventType).add(callback);
2652
+ logger.debug(`Event listener registered: ${eventType}`);
2595
2653
  }
2596
2654
  /**
2597
- * Get session ID
2655
+ * Remove an event listener
2598
2656
  */
2599
- getSessionId() {
2600
- return this.sessionId;
2657
+ off(eventType, callback) {
2658
+ const listeners = this.listeners.get(eventType);
2659
+ if (listeners) {
2660
+ listeners.delete(callback);
2661
+ }
2601
2662
  }
2602
2663
  /**
2603
- * Get workspace ID
2664
+ * Emit an event (client-side only)
2665
+ * This will trigger any registered local listeners
2604
2666
  */
2605
- getWorkspaceId() {
2606
- return this.workspaceId;
2667
+ emit(eventType, data) {
2668
+ logger.debug(`Event emitted: ${eventType}`, data);
2669
+ const listeners = this.listeners.get(eventType);
2670
+ if (listeners) {
2671
+ listeners.forEach(callback => {
2672
+ try {
2673
+ callback(data);
2674
+ }
2675
+ catch (error) {
2676
+ logger.error(`Error in event listener for ${eventType}:`, error);
2677
+ }
2678
+ });
2679
+ }
2607
2680
  }
2608
2681
  /**
2609
- * Get current configuration
2682
+ * Check if conditions are met for a trigger
2683
+ * Supports dynamic field evaluation including custom fields and nested paths
2610
2684
  */
2611
- getConfig() {
2612
- return { ...this.config };
2685
+ evaluateConditions(conditions, data) {
2686
+ if (!conditions || conditions.length === 0) {
2687
+ return true; // No conditions means always fire
2688
+ }
2689
+ return conditions.every(condition => {
2690
+ // Support dot notation for nested fields (e.g., 'customFields.industry')
2691
+ const fieldValue = condition.field.includes('.')
2692
+ ? this.getNestedValue(data, condition.field)
2693
+ : data[condition.field];
2694
+ const targetValue = condition.value;
2695
+ switch (condition.operator) {
2696
+ case 'equals':
2697
+ return fieldValue === targetValue;
2698
+ case 'not_equals':
2699
+ return fieldValue !== targetValue;
2700
+ case 'contains':
2701
+ return String(fieldValue).includes(String(targetValue));
2702
+ case 'greater_than':
2703
+ return Number(fieldValue) > Number(targetValue);
2704
+ case 'less_than':
2705
+ return Number(fieldValue) < Number(targetValue);
2706
+ case 'in':
2707
+ return Array.isArray(targetValue) && targetValue.includes(fieldValue);
2708
+ case 'not_in':
2709
+ return Array.isArray(targetValue) && !targetValue.includes(fieldValue);
2710
+ default:
2711
+ return false;
2712
+ }
2713
+ });
2613
2714
  }
2614
2715
  /**
2615
- * Force flush event queue
2716
+ * Execute actions for a triggered event (client-side preview)
2717
+ * Note: Actual execution happens on the backend
2616
2718
  */
2617
- async flush() {
2618
- await this.retryPendingIdentify();
2619
- await this.queue.flush();
2719
+ async executeActions(trigger, data) {
2720
+ logger.info(`Executing actions for trigger: ${trigger.name}`);
2721
+ for (const action of trigger.actions) {
2722
+ try {
2723
+ await this.executeAction(action, data);
2724
+ }
2725
+ catch (error) {
2726
+ logger.error(`Failed to execute action:`, error);
2727
+ }
2728
+ }
2620
2729
  }
2621
2730
  /**
2622
- * Reset visitor and session (for logout)
2731
+ * Execute a single action
2623
2732
  */
2624
- reset() {
2625
- logger.info('Resetting visitor data');
2626
- resetIds(this.config.useCookies);
2627
- this.visitorId = this.createVisitorId();
2628
- this.sessionId = this.createSessionId();
2629
- this.queue.clear();
2733
+ async executeAction(action, data) {
2734
+ switch (action.type) {
2735
+ case 'send_email':
2736
+ await this.executeSendEmail(action, data);
2737
+ break;
2738
+ case 'webhook':
2739
+ await this.executeWebhook(action, data);
2740
+ break;
2741
+ case 'create_task':
2742
+ await this.executeCreateTask(action, data);
2743
+ break;
2744
+ case 'update_contact':
2745
+ await this.executeUpdateContact(action, data);
2746
+ break;
2747
+ default:
2748
+ logger.warn(`Unknown action type:`, action);
2749
+ }
2630
2750
  }
2631
2751
  /**
2632
- * Delete all stored user data (GDPR right-to-erasure)
2752
+ * Execute send email action (via backend API)
2633
2753
  */
2634
- deleteData() {
2635
- logger.info('Deleting all user data (GDPR request)');
2636
- // Clear queue
2637
- this.queue.clear();
2638
- // Reset consent
2639
- this.consentManager.reset();
2640
- // Clear all stored IDs
2641
- resetIds(this.config.useCookies);
2642
- // Clear session storage items
2643
- if (typeof sessionStorage !== 'undefined') {
2644
- try {
2645
- sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
2646
- sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID + '_anon');
2647
- sessionStorage.removeItem(STORAGE_KEYS.SESSION_ID);
2648
- sessionStorage.removeItem(STORAGE_KEYS.SESSION_TIMESTAMP);
2649
- }
2650
- catch {
2651
- // Ignore errors
2652
- }
2653
- }
2654
- // Clear localStorage items
2655
- if (typeof localStorage !== 'undefined') {
2656
- try {
2657
- localStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
2658
- localStorage.removeItem(STORAGE_KEYS.CONSENT);
2659
- localStorage.removeItem(STORAGE_KEYS.EVENT_QUEUE);
2660
- }
2661
- catch {
2662
- // Ignore errors
2663
- }
2754
+ async executeSendEmail(action, data) {
2755
+ logger.debug('Sending email:', action);
2756
+ const payload = {
2757
+ to: this.replaceVariables(action.to, data),
2758
+ subject: action.subject ? this.replaceVariables(action.subject, data) : undefined,
2759
+ body: action.body ? this.replaceVariables(action.body, data) : undefined,
2760
+ templateId: action.templateId,
2761
+ cc: action.cc,
2762
+ bcc: action.bcc,
2763
+ from: action.from,
2764
+ delayMinutes: action.delayMinutes,
2765
+ };
2766
+ await this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
2767
+ method: 'POST',
2768
+ body: JSON.stringify(payload),
2769
+ });
2770
+ }
2771
+ /**
2772
+ * Execute webhook action
2773
+ */
2774
+ async executeWebhook(action, data) {
2775
+ logger.debug('Calling webhook:', action.url);
2776
+ const body = action.body ? this.replaceVariables(action.body, data) : JSON.stringify(data);
2777
+ await fetch(action.url, {
2778
+ method: action.method,
2779
+ headers: {
2780
+ 'Content-Type': 'application/json',
2781
+ ...action.headers,
2782
+ },
2783
+ body,
2784
+ });
2785
+ }
2786
+ /**
2787
+ * Execute create task action
2788
+ */
2789
+ async executeCreateTask(action, data) {
2790
+ logger.debug('Creating task:', action.title);
2791
+ const dueDate = action.dueDays
2792
+ ? new Date(Date.now() + action.dueDays * 24 * 60 * 60 * 1000).toISOString()
2793
+ : undefined;
2794
+ await this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
2795
+ method: 'POST',
2796
+ body: JSON.stringify({
2797
+ title: this.replaceVariables(action.title, data),
2798
+ description: action.description ? this.replaceVariables(action.description, data) : undefined,
2799
+ priority: action.priority,
2800
+ dueDate,
2801
+ assignedTo: action.assignedTo,
2802
+ relatedContactId: typeof data.contactId === 'string' ? data.contactId : undefined,
2803
+ }),
2804
+ });
2805
+ }
2806
+ /**
2807
+ * Execute update contact action
2808
+ */
2809
+ async executeUpdateContact(action, data) {
2810
+ const contactId = data.contactId || data._id;
2811
+ if (!contactId) {
2812
+ logger.warn('Cannot update contact: no contactId in data');
2813
+ return;
2664
2814
  }
2665
- // Generate new IDs
2666
- this.visitorId = this.createVisitorId();
2667
- this.sessionId = this.createSessionId();
2668
- logger.info('All user data deleted');
2815
+ logger.debug('Updating contact:', contactId);
2816
+ await this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2817
+ method: 'PUT',
2818
+ body: JSON.stringify(action.updates),
2819
+ });
2669
2820
  }
2670
2821
  /**
2671
- * Destroy tracker and cleanup
2822
+ * Replace variables in a string template
2823
+ * Supports syntax like {{contact.email}}, {{opportunity.value}}
2672
2824
  */
2673
- async destroy() {
2674
- logger.info('Destroying tracker');
2675
- // Flush any remaining events (await to ensure completion)
2676
- await this.queue.flush();
2677
- // Destroy plugins
2678
- for (const plugin of this.plugins) {
2679
- if (plugin.destroy) {
2680
- plugin.destroy();
2825
+ replaceVariables(template, data) {
2826
+ return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
2827
+ const value = this.getNestedValue(data, path.trim());
2828
+ return value !== undefined ? String(value) : match;
2829
+ });
2830
+ }
2831
+ /**
2832
+ * Get nested value from object using dot notation
2833
+ * Supports dynamic field access including custom fields
2834
+ */
2835
+ getNestedValue(obj, path) {
2836
+ return path.split('.').reduce((current, key) => {
2837
+ return current !== null && current !== undefined && typeof current === 'object'
2838
+ ? current[key]
2839
+ : undefined;
2840
+ }, obj);
2841
+ }
2842
+ /**
2843
+ * Extract all available field paths from a data object
2844
+ * Useful for dynamic field discovery based on platform-specific attributes
2845
+ * @param obj - The data object to extract fields from
2846
+ * @param prefix - Internal use for nested paths
2847
+ * @param maxDepth - Maximum depth to traverse (default: 3)
2848
+ * @returns Array of field paths (e.g., ['email', 'contact.firstName', 'customFields.industry'])
2849
+ */
2850
+ extractAvailableFields(obj, prefix = '', maxDepth = 3) {
2851
+ if (maxDepth <= 0)
2852
+ return [];
2853
+ const fields = [];
2854
+ for (const key in obj) {
2855
+ if (!obj.hasOwnProperty(key))
2856
+ continue;
2857
+ const value = obj[key];
2858
+ const fieldPath = prefix ? `${prefix}.${key}` : key;
2859
+ fields.push(fieldPath);
2860
+ // Recursively traverse nested objects
2861
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
2862
+ const nestedFields = this.extractAvailableFields(value, fieldPath, maxDepth - 1);
2863
+ fields.push(...nestedFields);
2681
2864
  }
2682
2865
  }
2683
- this.plugins = [];
2684
- // Destroy queue
2685
- this.queue.destroy();
2686
- this.isInitialized = false;
2866
+ return fields;
2867
+ }
2868
+ /**
2869
+ * Get available fields from sample data
2870
+ * Helps with dynamic field detection for platform-specific attributes
2871
+ * @param sampleData - Sample data object to analyze
2872
+ * @returns Array of available field paths
2873
+ */
2874
+ getAvailableFields(sampleData) {
2875
+ return this.extractAvailableFields(sampleData);
2876
+ }
2877
+ // ============================================
2878
+ // HELPER METHODS FOR COMMON PATTERNS
2879
+ // ============================================
2880
+ /**
2881
+ * Create a simple email trigger
2882
+ * Helper method for common use case
2883
+ */
2884
+ async createEmailTrigger(config) {
2885
+ return this.createTrigger({
2886
+ name: config.name,
2887
+ eventType: config.eventType,
2888
+ conditions: config.conditions,
2889
+ actions: [
2890
+ {
2891
+ type: 'send_email',
2892
+ to: config.to,
2893
+ subject: config.subject,
2894
+ body: config.body,
2895
+ },
2896
+ ],
2897
+ isActive: true,
2898
+ });
2899
+ }
2900
+ /**
2901
+ * Create a task creation trigger
2902
+ */
2903
+ async createTaskTrigger(config) {
2904
+ return this.createTrigger({
2905
+ name: config.name,
2906
+ eventType: config.eventType,
2907
+ conditions: config.conditions,
2908
+ actions: [
2909
+ {
2910
+ type: 'create_task',
2911
+ title: config.taskTitle,
2912
+ description: config.taskDescription,
2913
+ priority: config.priority,
2914
+ dueDays: config.dueDays,
2915
+ },
2916
+ ],
2917
+ isActive: true,
2918
+ });
2919
+ }
2920
+ /**
2921
+ * Create a webhook trigger
2922
+ */
2923
+ async createWebhookTrigger(config) {
2924
+ return this.createTrigger({
2925
+ name: config.name,
2926
+ eventType: config.eventType,
2927
+ conditions: config.conditions,
2928
+ actions: [
2929
+ {
2930
+ type: 'webhook',
2931
+ url: config.webhookUrl,
2932
+ method: config.method || 'POST',
2933
+ },
2934
+ ],
2935
+ isActive: true,
2936
+ });
2687
2937
  }
2688
2938
  }
2689
2939
 
2690
2940
  /**
2691
- * Clianta SDK - Event Triggers Manager
2692
- * Manages event-driven automation and email notifications
2941
+ * Clianta SDK - CRM API Client
2942
+ * @see SDK_VERSION in core/config.ts
2693
2943
  */
2694
2944
  /**
2695
- * Event Triggers Manager
2696
- * Handles event-driven automation based on CRM actions
2697
- *
2698
- * Similar to:
2699
- * - Salesforce: Process Builder, Flow Automation
2700
- * - HubSpot: Workflows, Email Sequences
2701
- * - Pipedrive: Workflow Automation
2945
+ * CRM API Client for managing contacts and opportunities
2702
2946
  */
2703
- class EventTriggersManager {
2704
- constructor(apiEndpoint, workspaceId, authToken) {
2705
- this.triggers = new Map();
2706
- this.listeners = new Map();
2947
+ class CRMClient {
2948
+ constructor(apiEndpoint, workspaceId, authToken, apiKey) {
2707
2949
  this.apiEndpoint = apiEndpoint;
2708
2950
  this.workspaceId = workspaceId;
2709
2951
  this.authToken = authToken;
2952
+ this.apiKey = apiKey;
2953
+ this.triggers = new EventTriggersManager(apiEndpoint, workspaceId, authToken);
2710
2954
  }
2711
2955
  /**
2712
- * Set authentication token
2956
+ * Set authentication token for API requests (user JWT)
2713
2957
  */
2714
2958
  setAuthToken(token) {
2715
2959
  this.authToken = token;
2960
+ this.apiKey = undefined;
2961
+ this.triggers.setAuthToken(token);
2962
+ }
2963
+ /**
2964
+ * Set workspace API key for server-to-server requests.
2965
+ * Use this instead of setAuthToken when integrating from an external app.
2966
+ */
2967
+ setApiKey(key) {
2968
+ this.apiKey = key;
2969
+ this.authToken = undefined;
2970
+ }
2971
+ /**
2972
+ * Validate required parameter exists
2973
+ * @throws {Error} if value is null/undefined or empty string
2974
+ */
2975
+ validateRequired(param, value, methodName) {
2976
+ if (value === null || value === undefined || value === '') {
2977
+ throw new Error(`[CRMClient.${methodName}] ${param} is required`);
2978
+ }
2716
2979
  }
2717
2980
  /**
2718
2981
  * Make authenticated API request
@@ -2723,970 +2986,1206 @@ class EventTriggersManager {
2723
2986
  'Content-Type': 'application/json',
2724
2987
  ...(options.headers || {}),
2725
2988
  };
2726
- if (this.authToken) {
2989
+ if (this.apiKey) {
2990
+ headers['X-Api-Key'] = this.apiKey;
2991
+ }
2992
+ else if (this.authToken) {
2993
+ headers['Authorization'] = `Bearer ${this.authToken}`;
2994
+ }
2995
+ try {
2996
+ const response = await fetch(url, {
2997
+ ...options,
2998
+ headers,
2999
+ });
3000
+ const data = await response.json();
3001
+ if (!response.ok) {
3002
+ return {
3003
+ success: false,
3004
+ error: data.message || 'Request failed',
3005
+ status: response.status,
3006
+ };
3007
+ }
3008
+ return {
3009
+ success: true,
3010
+ data: data.data || data,
3011
+ status: response.status,
3012
+ };
3013
+ }
3014
+ catch (error) {
3015
+ return {
3016
+ success: false,
3017
+ error: error instanceof Error ? error.message : 'Network error',
3018
+ status: 0,
3019
+ };
3020
+ }
3021
+ }
3022
+ // ============================================
3023
+ // INBOUND EVENTS API (API-key authenticated)
3024
+ // ============================================
3025
+ /**
3026
+ * Send an inbound event from an external app (e.g. user signup on client website).
3027
+ * Requires the client to be initialized with an API key via setApiKey() or the constructor.
3028
+ *
3029
+ * The contact is upserted in the CRM and matching workflow automations fire automatically.
3030
+ *
3031
+ * @example
3032
+ * const crm = new CRMClient('https://api.clianta.online', 'WORKSPACE_ID');
3033
+ * crm.setApiKey('mm_live_...');
3034
+ *
3035
+ * await crm.sendEvent({
3036
+ * event: 'user.registered',
3037
+ * contact: { email: 'alice@example.com', firstName: 'Alice' },
3038
+ * data: { plan: 'free', signupSource: 'homepage' },
3039
+ * });
3040
+ */
3041
+ async sendEvent(payload) {
3042
+ const url = `${this.apiEndpoint}/api/public/events`;
3043
+ const headers = { 'Content-Type': 'application/json' };
3044
+ if (this.apiKey) {
3045
+ headers['X-Api-Key'] = this.apiKey;
3046
+ }
3047
+ else if (this.authToken) {
2727
3048
  headers['Authorization'] = `Bearer ${this.authToken}`;
2728
3049
  }
2729
3050
  try {
2730
3051
  const response = await fetch(url, {
2731
- ...options,
3052
+ method: 'POST',
2732
3053
  headers,
3054
+ body: JSON.stringify(payload),
2733
3055
  });
2734
3056
  const data = await response.json();
2735
3057
  if (!response.ok) {
2736
3058
  return {
2737
3059
  success: false,
2738
- error: data.message || 'Request failed',
2739
- status: response.status,
3060
+ contactCreated: false,
3061
+ event: payload.event,
3062
+ error: data.error || 'Request failed',
2740
3063
  };
2741
3064
  }
2742
3065
  return {
2743
- success: true,
2744
- data: data.data || data,
2745
- status: response.status,
3066
+ success: data.success,
3067
+ contactCreated: data.contactCreated,
3068
+ contactId: data.contactId,
3069
+ event: data.event,
2746
3070
  };
2747
3071
  }
2748
3072
  catch (error) {
2749
3073
  return {
2750
3074
  success: false,
3075
+ contactCreated: false,
3076
+ event: payload.event,
2751
3077
  error: error instanceof Error ? error.message : 'Network error',
2752
- status: 0,
2753
3078
  };
2754
3079
  }
2755
3080
  }
2756
3081
  // ============================================
2757
- // TRIGGER MANAGEMENT
3082
+ // CONTACTS API
2758
3083
  // ============================================
2759
3084
  /**
2760
- * Get all event triggers
3085
+ * Get all contacts with pagination
2761
3086
  */
2762
- async getTriggers() {
2763
- return this.request(`/api/workspaces/${this.workspaceId}/triggers`);
3087
+ async getContacts(params) {
3088
+ const queryParams = new URLSearchParams();
3089
+ if (params?.page)
3090
+ queryParams.set('page', params.page.toString());
3091
+ if (params?.limit)
3092
+ queryParams.set('limit', params.limit.toString());
3093
+ if (params?.search)
3094
+ queryParams.set('search', params.search);
3095
+ if (params?.status)
3096
+ queryParams.set('status', params.status);
3097
+ const query = queryParams.toString();
3098
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts${query ? `?${query}` : ''}`;
3099
+ return this.request(endpoint);
2764
3100
  }
2765
3101
  /**
2766
- * Get a single trigger by ID
3102
+ * Get a single contact by ID
2767
3103
  */
2768
- async getTrigger(triggerId) {
2769
- return this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`);
3104
+ async getContact(contactId) {
3105
+ this.validateRequired('contactId', contactId, 'getContact');
3106
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`);
2770
3107
  }
2771
3108
  /**
2772
- * Create a new event trigger
3109
+ * Create a new contact
2773
3110
  */
2774
- async createTrigger(trigger) {
2775
- const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers`, {
3111
+ async createContact(contact) {
3112
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts`, {
2776
3113
  method: 'POST',
2777
- body: JSON.stringify(trigger),
3114
+ body: JSON.stringify(contact),
2778
3115
  });
2779
- // Cache the trigger locally if successful
2780
- if (result.success && result.data?._id) {
2781
- this.triggers.set(result.data._id, result.data);
2782
- }
2783
- return result;
2784
3116
  }
2785
3117
  /**
2786
- * Update an existing trigger
3118
+ * Update an existing contact
2787
3119
  */
2788
- async updateTrigger(triggerId, updates) {
2789
- const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
3120
+ async updateContact(contactId, updates) {
3121
+ this.validateRequired('contactId', contactId, 'updateContact');
3122
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2790
3123
  method: 'PUT',
2791
3124
  body: JSON.stringify(updates),
2792
3125
  });
2793
- // Update cache if successful
2794
- if (result.success && result.data?._id) {
2795
- this.triggers.set(result.data._id, result.data);
2796
- }
2797
- return result;
2798
3126
  }
2799
3127
  /**
2800
- * Delete a trigger
3128
+ * Delete a contact
2801
3129
  */
2802
- async deleteTrigger(triggerId) {
2803
- const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
3130
+ async deleteContact(contactId) {
3131
+ this.validateRequired('contactId', contactId, 'deleteContact');
3132
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2804
3133
  method: 'DELETE',
2805
3134
  });
2806
- // Remove from cache if successful
2807
- if (result.success) {
2808
- this.triggers.delete(triggerId);
2809
- }
2810
- return result;
2811
- }
2812
- /**
2813
- * Activate a trigger
2814
- */
2815
- async activateTrigger(triggerId) {
2816
- return this.updateTrigger(triggerId, { isActive: true });
2817
- }
2818
- /**
2819
- * Deactivate a trigger
2820
- */
2821
- async deactivateTrigger(triggerId) {
2822
- return this.updateTrigger(triggerId, { isActive: false });
2823
3135
  }
2824
3136
  // ============================================
2825
- // EVENT HANDLING (CLIENT-SIDE)
3137
+ // OPPORTUNITIES API
2826
3138
  // ============================================
2827
3139
  /**
2828
- * Register a local event listener for client-side triggers
2829
- * This allows immediate client-side reactions to events
2830
- */
2831
- on(eventType, callback) {
2832
- if (!this.listeners.has(eventType)) {
2833
- this.listeners.set(eventType, new Set());
2834
- }
2835
- this.listeners.get(eventType).add(callback);
2836
- logger.debug(`Event listener registered: ${eventType}`);
2837
- }
2838
- /**
2839
- * Remove an event listener
3140
+ * Get all opportunities with pagination
2840
3141
  */
2841
- off(eventType, callback) {
2842
- const listeners = this.listeners.get(eventType);
2843
- if (listeners) {
2844
- listeners.delete(callback);
2845
- }
3142
+ async getOpportunities(params) {
3143
+ const queryParams = new URLSearchParams();
3144
+ if (params?.page)
3145
+ queryParams.set('page', params.page.toString());
3146
+ if (params?.limit)
3147
+ queryParams.set('limit', params.limit.toString());
3148
+ if (params?.pipelineId)
3149
+ queryParams.set('pipelineId', params.pipelineId);
3150
+ if (params?.stageId)
3151
+ queryParams.set('stageId', params.stageId);
3152
+ const query = queryParams.toString();
3153
+ const endpoint = `/api/workspaces/${this.workspaceId}/opportunities${query ? `?${query}` : ''}`;
3154
+ return this.request(endpoint);
2846
3155
  }
2847
3156
  /**
2848
- * Emit an event (client-side only)
2849
- * This will trigger any registered local listeners
3157
+ * Get a single opportunity by ID
2850
3158
  */
2851
- emit(eventType, data) {
2852
- logger.debug(`Event emitted: ${eventType}`, data);
2853
- const listeners = this.listeners.get(eventType);
2854
- if (listeners) {
2855
- listeners.forEach(callback => {
2856
- try {
2857
- callback(data);
2858
- }
2859
- catch (error) {
2860
- logger.error(`Error in event listener for ${eventType}:`, error);
2861
- }
2862
- });
2863
- }
3159
+ async getOpportunity(opportunityId) {
3160
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`);
2864
3161
  }
2865
3162
  /**
2866
- * Check if conditions are met for a trigger
2867
- * Supports dynamic field evaluation including custom fields and nested paths
3163
+ * Create a new opportunity
2868
3164
  */
2869
- evaluateConditions(conditions, data) {
2870
- if (!conditions || conditions.length === 0) {
2871
- return true; // No conditions means always fire
2872
- }
2873
- return conditions.every(condition => {
2874
- // Support dot notation for nested fields (e.g., 'customFields.industry')
2875
- const fieldValue = condition.field.includes('.')
2876
- ? this.getNestedValue(data, condition.field)
2877
- : data[condition.field];
2878
- const targetValue = condition.value;
2879
- switch (condition.operator) {
2880
- case 'equals':
2881
- return fieldValue === targetValue;
2882
- case 'not_equals':
2883
- return fieldValue !== targetValue;
2884
- case 'contains':
2885
- return String(fieldValue).includes(String(targetValue));
2886
- case 'greater_than':
2887
- return Number(fieldValue) > Number(targetValue);
2888
- case 'less_than':
2889
- return Number(fieldValue) < Number(targetValue);
2890
- case 'in':
2891
- return Array.isArray(targetValue) && targetValue.includes(fieldValue);
2892
- case 'not_in':
2893
- return Array.isArray(targetValue) && !targetValue.includes(fieldValue);
2894
- default:
2895
- return false;
2896
- }
3165
+ async createOpportunity(opportunity) {
3166
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities`, {
3167
+ method: 'POST',
3168
+ body: JSON.stringify(opportunity),
2897
3169
  });
2898
3170
  }
2899
3171
  /**
2900
- * Execute actions for a triggered event (client-side preview)
2901
- * Note: Actual execution happens on the backend
3172
+ * Update an existing opportunity
2902
3173
  */
2903
- async executeActions(trigger, data) {
2904
- logger.info(`Executing actions for trigger: ${trigger.name}`);
2905
- for (const action of trigger.actions) {
2906
- try {
2907
- await this.executeAction(action, data);
2908
- }
2909
- catch (error) {
2910
- logger.error(`Failed to execute action:`, error);
2911
- }
2912
- }
3174
+ async updateOpportunity(opportunityId, updates) {
3175
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
3176
+ method: 'PUT',
3177
+ body: JSON.stringify(updates),
3178
+ });
2913
3179
  }
2914
3180
  /**
2915
- * Execute a single action
3181
+ * Delete an opportunity
2916
3182
  */
2917
- async executeAction(action, data) {
2918
- switch (action.type) {
2919
- case 'send_email':
2920
- await this.executeSendEmail(action, data);
2921
- break;
2922
- case 'webhook':
2923
- await this.executeWebhook(action, data);
2924
- break;
2925
- case 'create_task':
2926
- await this.executeCreateTask(action, data);
2927
- break;
2928
- case 'update_contact':
2929
- await this.executeUpdateContact(action, data);
2930
- break;
2931
- default:
2932
- logger.warn(`Unknown action type:`, action);
2933
- }
3183
+ async deleteOpportunity(opportunityId) {
3184
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
3185
+ method: 'DELETE',
3186
+ });
2934
3187
  }
2935
3188
  /**
2936
- * Execute send email action (via backend API)
3189
+ * Move opportunity to a different stage
2937
3190
  */
2938
- async executeSendEmail(action, data) {
2939
- logger.debug('Sending email:', action);
2940
- const payload = {
2941
- to: this.replaceVariables(action.to, data),
2942
- subject: action.subject ? this.replaceVariables(action.subject, data) : undefined,
2943
- body: action.body ? this.replaceVariables(action.body, data) : undefined,
2944
- templateId: action.templateId,
2945
- cc: action.cc,
2946
- bcc: action.bcc,
2947
- from: action.from,
2948
- delayMinutes: action.delayMinutes,
2949
- };
2950
- await this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
3191
+ async moveOpportunity(opportunityId, stageId) {
3192
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/move`, {
2951
3193
  method: 'POST',
2952
- body: JSON.stringify(payload),
3194
+ body: JSON.stringify({ stageId }),
2953
3195
  });
2954
3196
  }
3197
+ // ============================================
3198
+ // COMPANIES API
3199
+ // ============================================
2955
3200
  /**
2956
- * Execute webhook action
3201
+ * Get all companies with pagination
2957
3202
  */
2958
- async executeWebhook(action, data) {
2959
- logger.debug('Calling webhook:', action.url);
2960
- const body = action.body ? this.replaceVariables(action.body, data) : JSON.stringify(data);
2961
- await fetch(action.url, {
2962
- method: action.method,
2963
- headers: {
2964
- 'Content-Type': 'application/json',
2965
- ...action.headers,
2966
- },
2967
- body,
2968
- });
3203
+ async getCompanies(params) {
3204
+ const queryParams = new URLSearchParams();
3205
+ if (params?.page)
3206
+ queryParams.set('page', params.page.toString());
3207
+ if (params?.limit)
3208
+ queryParams.set('limit', params.limit.toString());
3209
+ if (params?.search)
3210
+ queryParams.set('search', params.search);
3211
+ if (params?.status)
3212
+ queryParams.set('status', params.status);
3213
+ if (params?.industry)
3214
+ queryParams.set('industry', params.industry);
3215
+ const query = queryParams.toString();
3216
+ const endpoint = `/api/workspaces/${this.workspaceId}/companies${query ? `?${query}` : ''}`;
3217
+ return this.request(endpoint);
2969
3218
  }
2970
3219
  /**
2971
- * Execute create task action
3220
+ * Get a single company by ID
2972
3221
  */
2973
- async executeCreateTask(action, data) {
2974
- logger.debug('Creating task:', action.title);
2975
- const dueDate = action.dueDays
2976
- ? new Date(Date.now() + action.dueDays * 24 * 60 * 60 * 1000).toISOString()
2977
- : undefined;
2978
- await this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
2979
- method: 'POST',
2980
- body: JSON.stringify({
2981
- title: this.replaceVariables(action.title, data),
2982
- description: action.description ? this.replaceVariables(action.description, data) : undefined,
2983
- priority: action.priority,
2984
- dueDate,
2985
- assignedTo: action.assignedTo,
2986
- relatedContactId: typeof data.contactId === 'string' ? data.contactId : undefined,
2987
- }),
2988
- });
3222
+ async getCompany(companyId) {
3223
+ return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`);
2989
3224
  }
2990
3225
  /**
2991
- * Execute update contact action
3226
+ * Create a new company
2992
3227
  */
2993
- async executeUpdateContact(action, data) {
2994
- const contactId = data.contactId || data._id;
2995
- if (!contactId) {
2996
- logger.warn('Cannot update contact: no contactId in data');
2997
- return;
2998
- }
2999
- logger.debug('Updating contact:', contactId);
3000
- await this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
3001
- method: 'PUT',
3002
- body: JSON.stringify(action.updates),
3228
+ async createCompany(company) {
3229
+ return this.request(`/api/workspaces/${this.workspaceId}/companies`, {
3230
+ method: 'POST',
3231
+ body: JSON.stringify(company),
3003
3232
  });
3004
3233
  }
3005
3234
  /**
3006
- * Replace variables in a string template
3007
- * Supports syntax like {{contact.email}}, {{opportunity.value}}
3235
+ * Update an existing company
3008
3236
  */
3009
- replaceVariables(template, data) {
3010
- return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
3011
- const value = this.getNestedValue(data, path.trim());
3012
- return value !== undefined ? String(value) : match;
3237
+ async updateCompany(companyId, updates) {
3238
+ return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
3239
+ method: 'PUT',
3240
+ body: JSON.stringify(updates),
3013
3241
  });
3014
3242
  }
3015
3243
  /**
3016
- * Get nested value from object using dot notation
3017
- * Supports dynamic field access including custom fields
3244
+ * Delete a company
3018
3245
  */
3019
- getNestedValue(obj, path) {
3020
- return path.split('.').reduce((current, key) => {
3021
- return current !== null && current !== undefined && typeof current === 'object'
3022
- ? current[key]
3023
- : undefined;
3024
- }, obj);
3246
+ async deleteCompany(companyId) {
3247
+ return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
3248
+ method: 'DELETE',
3249
+ });
3025
3250
  }
3026
3251
  /**
3027
- * Extract all available field paths from a data object
3028
- * Useful for dynamic field discovery based on platform-specific attributes
3029
- * @param obj - The data object to extract fields from
3030
- * @param prefix - Internal use for nested paths
3031
- * @param maxDepth - Maximum depth to traverse (default: 3)
3032
- * @returns Array of field paths (e.g., ['email', 'contact.firstName', 'customFields.industry'])
3252
+ * Get contacts belonging to a company
3033
3253
  */
3034
- extractAvailableFields(obj, prefix = '', maxDepth = 3) {
3035
- if (maxDepth <= 0)
3036
- return [];
3037
- const fields = [];
3038
- for (const key in obj) {
3039
- if (!obj.hasOwnProperty(key))
3040
- continue;
3041
- const value = obj[key];
3042
- const fieldPath = prefix ? `${prefix}.${key}` : key;
3043
- fields.push(fieldPath);
3044
- // Recursively traverse nested objects
3045
- if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
3046
- const nestedFields = this.extractAvailableFields(value, fieldPath, maxDepth - 1);
3047
- fields.push(...nestedFields);
3048
- }
3049
- }
3050
- return fields;
3254
+ async getCompanyContacts(companyId, params) {
3255
+ const queryParams = new URLSearchParams();
3256
+ if (params?.page)
3257
+ queryParams.set('page', params.page.toString());
3258
+ if (params?.limit)
3259
+ queryParams.set('limit', params.limit.toString());
3260
+ const query = queryParams.toString();
3261
+ const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/contacts${query ? `?${query}` : ''}`;
3262
+ return this.request(endpoint);
3051
3263
  }
3052
3264
  /**
3053
- * Get available fields from sample data
3054
- * Helps with dynamic field detection for platform-specific attributes
3055
- * @param sampleData - Sample data object to analyze
3056
- * @returns Array of available field paths
3265
+ * Get deals/opportunities belonging to a company
3057
3266
  */
3058
- getAvailableFields(sampleData) {
3059
- return this.extractAvailableFields(sampleData);
3267
+ async getCompanyDeals(companyId, params) {
3268
+ const queryParams = new URLSearchParams();
3269
+ if (params?.page)
3270
+ queryParams.set('page', params.page.toString());
3271
+ if (params?.limit)
3272
+ queryParams.set('limit', params.limit.toString());
3273
+ const query = queryParams.toString();
3274
+ const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/deals${query ? `?${query}` : ''}`;
3275
+ return this.request(endpoint);
3060
3276
  }
3061
3277
  // ============================================
3062
- // HELPER METHODS FOR COMMON PATTERNS
3278
+ // PIPELINES API
3063
3279
  // ============================================
3064
3280
  /**
3065
- * Create a simple email trigger
3066
- * Helper method for common use case
3281
+ * Get all pipelines
3067
3282
  */
3068
- async createEmailTrigger(config) {
3069
- return this.createTrigger({
3070
- name: config.name,
3071
- eventType: config.eventType,
3072
- conditions: config.conditions,
3073
- actions: [
3074
- {
3075
- type: 'send_email',
3076
- to: config.to,
3077
- subject: config.subject,
3078
- body: config.body,
3079
- },
3080
- ],
3081
- isActive: true,
3082
- });
3283
+ async getPipelines() {
3284
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines`);
3083
3285
  }
3084
3286
  /**
3085
- * Create a task creation trigger
3287
+ * Get a single pipeline by ID
3086
3288
  */
3087
- async createTaskTrigger(config) {
3088
- return this.createTrigger({
3089
- name: config.name,
3090
- eventType: config.eventType,
3091
- conditions: config.conditions,
3092
- actions: [
3093
- {
3094
- type: 'create_task',
3095
- title: config.taskTitle,
3096
- description: config.taskDescription,
3097
- priority: config.priority,
3098
- dueDays: config.dueDays,
3099
- },
3100
- ],
3101
- isActive: true,
3102
- });
3289
+ async getPipeline(pipelineId) {
3290
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`);
3103
3291
  }
3104
3292
  /**
3105
- * Create a webhook trigger
3293
+ * Create a new pipeline
3106
3294
  */
3107
- async createWebhookTrigger(config) {
3108
- return this.createTrigger({
3109
- name: config.name,
3110
- eventType: config.eventType,
3111
- conditions: config.conditions,
3112
- actions: [
3113
- {
3114
- type: 'webhook',
3115
- url: config.webhookUrl,
3116
- method: config.method || 'POST',
3117
- },
3118
- ],
3119
- isActive: true,
3295
+ async createPipeline(pipeline) {
3296
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines`, {
3297
+ method: 'POST',
3298
+ body: JSON.stringify(pipeline),
3120
3299
  });
3121
3300
  }
3122
- }
3123
-
3124
- /**
3125
- * Clianta SDK - CRM API Client
3126
- * @see SDK_VERSION in core/config.ts
3127
- */
3128
- /**
3129
- * CRM API Client for managing contacts and opportunities
3130
- */
3131
- class CRMClient {
3132
- constructor(apiEndpoint, workspaceId, authToken) {
3133
- this.apiEndpoint = apiEndpoint;
3134
- this.workspaceId = workspaceId;
3135
- this.authToken = authToken;
3136
- this.triggers = new EventTriggersManager(apiEndpoint, workspaceId, authToken);
3137
- }
3138
- /**
3139
- * Set authentication token for API requests
3140
- */
3141
- setAuthToken(token) {
3142
- this.authToken = token;
3143
- this.triggers.setAuthToken(token);
3144
- }
3145
3301
  /**
3146
- * Validate required parameter exists
3147
- * @throws {Error} if value is null/undefined or empty string
3302
+ * Update an existing pipeline
3148
3303
  */
3149
- validateRequired(param, value, methodName) {
3150
- if (value === null || value === undefined || value === '') {
3151
- throw new Error(`[CRMClient.${methodName}] ${param} is required`);
3152
- }
3304
+ async updatePipeline(pipelineId, updates) {
3305
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
3306
+ method: 'PUT',
3307
+ body: JSON.stringify(updates),
3308
+ });
3153
3309
  }
3154
3310
  /**
3155
- * Make authenticated API request
3311
+ * Delete a pipeline
3156
3312
  */
3157
- async request(endpoint, options = {}) {
3158
- const url = `${this.apiEndpoint}${endpoint}`;
3159
- const headers = {
3160
- 'Content-Type': 'application/json',
3161
- ...(options.headers || {}),
3162
- };
3163
- if (this.authToken) {
3164
- headers['Authorization'] = `Bearer ${this.authToken}`;
3165
- }
3166
- try {
3167
- const response = await fetch(url, {
3168
- ...options,
3169
- headers,
3170
- });
3171
- const data = await response.json();
3172
- if (!response.ok) {
3173
- return {
3174
- success: false,
3175
- error: data.message || 'Request failed',
3176
- status: response.status,
3177
- };
3178
- }
3179
- return {
3180
- success: true,
3181
- data: data.data || data,
3182
- status: response.status,
3183
- };
3184
- }
3185
- catch (error) {
3186
- return {
3187
- success: false,
3188
- error: error instanceof Error ? error.message : 'Network error',
3189
- status: 0,
3190
- };
3191
- }
3313
+ async deletePipeline(pipelineId) {
3314
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
3315
+ method: 'DELETE',
3316
+ });
3192
3317
  }
3193
3318
  // ============================================
3194
- // CONTACTS API
3319
+ // TASKS API
3195
3320
  // ============================================
3196
3321
  /**
3197
- * Get all contacts with pagination
3322
+ * Get all tasks with pagination
3198
3323
  */
3199
- async getContacts(params) {
3324
+ async getTasks(params) {
3200
3325
  const queryParams = new URLSearchParams();
3201
3326
  if (params?.page)
3202
3327
  queryParams.set('page', params.page.toString());
3203
3328
  if (params?.limit)
3204
3329
  queryParams.set('limit', params.limit.toString());
3205
- if (params?.search)
3206
- queryParams.set('search', params.search);
3207
3330
  if (params?.status)
3208
3331
  queryParams.set('status', params.status);
3332
+ if (params?.priority)
3333
+ queryParams.set('priority', params.priority);
3334
+ if (params?.contactId)
3335
+ queryParams.set('contactId', params.contactId);
3336
+ if (params?.companyId)
3337
+ queryParams.set('companyId', params.companyId);
3338
+ if (params?.opportunityId)
3339
+ queryParams.set('opportunityId', params.opportunityId);
3209
3340
  const query = queryParams.toString();
3210
- const endpoint = `/api/workspaces/${this.workspaceId}/contacts${query ? `?${query}` : ''}`;
3341
+ const endpoint = `/api/workspaces/${this.workspaceId}/tasks${query ? `?${query}` : ''}`;
3211
3342
  return this.request(endpoint);
3212
3343
  }
3213
3344
  /**
3214
- * Get a single contact by ID
3345
+ * Get a single task by ID
3215
3346
  */
3216
- async getContact(contactId) {
3217
- this.validateRequired('contactId', contactId, 'getContact');
3218
- return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`);
3347
+ async getTask(taskId) {
3348
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`);
3219
3349
  }
3220
3350
  /**
3221
- * Create a new contact
3351
+ * Create a new task
3222
3352
  */
3223
- async createContact(contact) {
3224
- return this.request(`/api/workspaces/${this.workspaceId}/contacts`, {
3353
+ async createTask(task) {
3354
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
3225
3355
  method: 'POST',
3226
- body: JSON.stringify(contact),
3356
+ body: JSON.stringify(task),
3227
3357
  });
3228
3358
  }
3229
3359
  /**
3230
- * Update an existing contact
3360
+ * Update an existing task
3231
3361
  */
3232
- async updateContact(contactId, updates) {
3233
- this.validateRequired('contactId', contactId, 'updateContact');
3234
- return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
3362
+ async updateTask(taskId, updates) {
3363
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
3235
3364
  method: 'PUT',
3236
3365
  body: JSON.stringify(updates),
3237
3366
  });
3238
3367
  }
3239
3368
  /**
3240
- * Delete a contact
3369
+ * Mark a task as completed
3241
3370
  */
3242
- async deleteContact(contactId) {
3243
- this.validateRequired('contactId', contactId, 'deleteContact');
3244
- return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
3371
+ async completeTask(taskId) {
3372
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}/complete`, {
3373
+ method: 'PATCH',
3374
+ });
3375
+ }
3376
+ /**
3377
+ * Delete a task
3378
+ */
3379
+ async deleteTask(taskId) {
3380
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
3245
3381
  method: 'DELETE',
3246
3382
  });
3247
3383
  }
3248
3384
  // ============================================
3249
- // OPPORTUNITIES API
3385
+ // ACTIVITIES API
3250
3386
  // ============================================
3251
3387
  /**
3252
- * Get all opportunities with pagination
3388
+ * Get activities for a contact
3253
3389
  */
3254
- async getOpportunities(params) {
3390
+ async getContactActivities(contactId, params) {
3255
3391
  const queryParams = new URLSearchParams();
3256
3392
  if (params?.page)
3257
3393
  queryParams.set('page', params.page.toString());
3258
3394
  if (params?.limit)
3259
3395
  queryParams.set('limit', params.limit.toString());
3260
- if (params?.pipelineId)
3261
- queryParams.set('pipelineId', params.pipelineId);
3262
- if (params?.stageId)
3263
- queryParams.set('stageId', params.stageId);
3396
+ if (params?.type)
3397
+ queryParams.set('type', params.type);
3264
3398
  const query = queryParams.toString();
3265
- const endpoint = `/api/workspaces/${this.workspaceId}/opportunities${query ? `?${query}` : ''}`;
3399
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
3266
3400
  return this.request(endpoint);
3267
3401
  }
3268
3402
  /**
3269
- * Get a single opportunity by ID
3403
+ * Get activities for an opportunity/deal
3270
3404
  */
3271
- async getOpportunity(opportunityId) {
3272
- return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`);
3405
+ async getOpportunityActivities(opportunityId, params) {
3406
+ const queryParams = new URLSearchParams();
3407
+ if (params?.page)
3408
+ queryParams.set('page', params.page.toString());
3409
+ if (params?.limit)
3410
+ queryParams.set('limit', params.limit.toString());
3411
+ if (params?.type)
3412
+ queryParams.set('type', params.type);
3413
+ const query = queryParams.toString();
3414
+ const endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/activities${query ? `?${query}` : ''}`;
3415
+ return this.request(endpoint);
3273
3416
  }
3274
3417
  /**
3275
- * Create a new opportunity
3418
+ * Create a new activity
3276
3419
  */
3277
- async createOpportunity(opportunity) {
3278
- return this.request(`/api/workspaces/${this.workspaceId}/opportunities`, {
3420
+ async createActivity(activity) {
3421
+ // Determine the correct endpoint based on related entity
3422
+ let endpoint;
3423
+ if (activity.opportunityId) {
3424
+ endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${activity.opportunityId}/activities`;
3425
+ }
3426
+ else if (activity.contactId) {
3427
+ endpoint = `/api/workspaces/${this.workspaceId}/contacts/${activity.contactId}/activities`;
3428
+ }
3429
+ else {
3430
+ endpoint = `/api/workspaces/${this.workspaceId}/activities`;
3431
+ }
3432
+ return this.request(endpoint, {
3279
3433
  method: 'POST',
3280
- body: JSON.stringify(opportunity),
3434
+ body: JSON.stringify(activity),
3281
3435
  });
3282
3436
  }
3283
3437
  /**
3284
- * Update an existing opportunity
3438
+ * Update an existing activity
3285
3439
  */
3286
- async updateOpportunity(opportunityId, updates) {
3287
- return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
3288
- method: 'PUT',
3440
+ async updateActivity(activityId, updates) {
3441
+ return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
3442
+ method: 'PATCH',
3289
3443
  body: JSON.stringify(updates),
3290
3444
  });
3291
3445
  }
3292
3446
  /**
3293
- * Delete an opportunity
3447
+ * Delete an activity
3294
3448
  */
3295
- async deleteOpportunity(opportunityId) {
3296
- return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
3449
+ async deleteActivity(activityId) {
3450
+ return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
3297
3451
  method: 'DELETE',
3298
3452
  });
3299
3453
  }
3300
3454
  /**
3301
- * Move opportunity to a different stage
3455
+ * Log a call activity
3302
3456
  */
3303
- async moveOpportunity(opportunityId, stageId) {
3304
- return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/move`, {
3305
- method: 'POST',
3306
- body: JSON.stringify({ stageId }),
3457
+ async logCall(data) {
3458
+ return this.createActivity({
3459
+ type: 'call',
3460
+ title: `${data.direction === 'inbound' ? 'Inbound' : 'Outbound'} Call`,
3461
+ direction: data.direction,
3462
+ duration: data.duration,
3463
+ outcome: data.outcome,
3464
+ description: data.notes,
3465
+ contactId: data.contactId,
3466
+ opportunityId: data.opportunityId,
3467
+ });
3468
+ }
3469
+ /**
3470
+ * Log a meeting activity
3471
+ */
3472
+ async logMeeting(data) {
3473
+ return this.createActivity({
3474
+ type: 'meeting',
3475
+ title: data.title,
3476
+ duration: data.duration,
3477
+ outcome: data.outcome,
3478
+ description: data.notes,
3479
+ contactId: data.contactId,
3480
+ opportunityId: data.opportunityId,
3481
+ });
3482
+ }
3483
+ /**
3484
+ * Add a note to a contact or opportunity
3485
+ */
3486
+ async addNote(data) {
3487
+ return this.createActivity({
3488
+ type: 'note',
3489
+ title: 'Note',
3490
+ description: data.content,
3491
+ contactId: data.contactId,
3492
+ opportunityId: data.opportunityId,
3307
3493
  });
3308
3494
  }
3309
3495
  // ============================================
3310
- // COMPANIES API
3496
+ // EMAIL TEMPLATES API
3311
3497
  // ============================================
3312
3498
  /**
3313
- * Get all companies with pagination
3499
+ * Get all email templates
3314
3500
  */
3315
- async getCompanies(params) {
3501
+ async getEmailTemplates(params) {
3316
3502
  const queryParams = new URLSearchParams();
3317
3503
  if (params?.page)
3318
3504
  queryParams.set('page', params.page.toString());
3319
3505
  if (params?.limit)
3320
3506
  queryParams.set('limit', params.limit.toString());
3321
- if (params?.search)
3322
- queryParams.set('search', params.search);
3323
- if (params?.status)
3324
- queryParams.set('status', params.status);
3325
- if (params?.industry)
3326
- queryParams.set('industry', params.industry);
3327
3507
  const query = queryParams.toString();
3328
- const endpoint = `/api/workspaces/${this.workspaceId}/companies${query ? `?${query}` : ''}`;
3508
+ const endpoint = `/api/workspaces/${this.workspaceId}/email-templates${query ? `?${query}` : ''}`;
3329
3509
  return this.request(endpoint);
3330
3510
  }
3331
3511
  /**
3332
- * Get a single company by ID
3512
+ * Get a single email template by ID
3333
3513
  */
3334
- async getCompany(companyId) {
3335
- return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`);
3514
+ async getEmailTemplate(templateId) {
3515
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`);
3336
3516
  }
3337
3517
  /**
3338
- * Create a new company
3518
+ * Create a new email template
3339
3519
  */
3340
- async createCompany(company) {
3341
- return this.request(`/api/workspaces/${this.workspaceId}/companies`, {
3520
+ async createEmailTemplate(template) {
3521
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates`, {
3342
3522
  method: 'POST',
3343
- body: JSON.stringify(company),
3523
+ body: JSON.stringify(template),
3344
3524
  });
3345
3525
  }
3346
3526
  /**
3347
- * Update an existing company
3527
+ * Update an email template
3348
3528
  */
3349
- async updateCompany(companyId, updates) {
3350
- return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
3529
+ async updateEmailTemplate(templateId, updates) {
3530
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
3351
3531
  method: 'PUT',
3352
3532
  body: JSON.stringify(updates),
3353
3533
  });
3354
3534
  }
3355
3535
  /**
3356
- * Delete a company
3536
+ * Delete an email template
3357
3537
  */
3358
- async deleteCompany(companyId) {
3359
- return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
3538
+ async deleteEmailTemplate(templateId) {
3539
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
3360
3540
  method: 'DELETE',
3361
3541
  });
3362
3542
  }
3363
3543
  /**
3364
- * Get contacts belonging to a company
3544
+ * Send an email using a template
3365
3545
  */
3366
- async getCompanyContacts(companyId, params) {
3546
+ async sendEmail(data) {
3547
+ return this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
3548
+ method: 'POST',
3549
+ body: JSON.stringify(data),
3550
+ });
3551
+ }
3552
+ // ============================================
3553
+ // READ-BACK / DATA RETRIEVAL API
3554
+ // ============================================
3555
+ /**
3556
+ * Get a contact by email address.
3557
+ * Returns the first matching contact from a search query.
3558
+ */
3559
+ async getContactByEmail(email) {
3560
+ this.validateRequired('email', email, 'getContactByEmail');
3561
+ const queryParams = new URLSearchParams({ search: email, limit: '1' });
3562
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts?${queryParams.toString()}`);
3563
+ }
3564
+ /**
3565
+ * Get activity timeline for a contact
3566
+ */
3567
+ async getContactActivity(contactId, params) {
3568
+ this.validateRequired('contactId', contactId, 'getContactActivity');
3367
3569
  const queryParams = new URLSearchParams();
3368
3570
  if (params?.page)
3369
3571
  queryParams.set('page', params.page.toString());
3370
3572
  if (params?.limit)
3371
3573
  queryParams.set('limit', params.limit.toString());
3574
+ if (params?.type)
3575
+ queryParams.set('type', params.type);
3576
+ if (params?.startDate)
3577
+ queryParams.set('startDate', params.startDate);
3578
+ if (params?.endDate)
3579
+ queryParams.set('endDate', params.endDate);
3372
3580
  const query = queryParams.toString();
3373
- const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/contacts${query ? `?${query}` : ''}`;
3581
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
3374
3582
  return this.request(endpoint);
3375
3583
  }
3376
3584
  /**
3377
- * Get deals/opportunities belonging to a company
3585
+ * Get engagement metrics for a contact (via their linked visitor data)
3378
3586
  */
3379
- async getCompanyDeals(companyId, params) {
3587
+ async getContactEngagement(contactId) {
3588
+ this.validateRequired('contactId', contactId, 'getContactEngagement');
3589
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}/engagement`);
3590
+ }
3591
+ /**
3592
+ * Get a full timeline for a contact including events, activities, and opportunities
3593
+ */
3594
+ async getContactTimeline(contactId, params) {
3595
+ this.validateRequired('contactId', contactId, 'getContactTimeline');
3380
3596
  const queryParams = new URLSearchParams();
3381
3597
  if (params?.page)
3382
3598
  queryParams.set('page', params.page.toString());
3383
3599
  if (params?.limit)
3384
3600
  queryParams.set('limit', params.limit.toString());
3385
3601
  const query = queryParams.toString();
3386
- const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/deals${query ? `?${query}` : ''}`;
3602
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/timeline${query ? `?${query}` : ''}`;
3603
+ return this.request(endpoint);
3604
+ }
3605
+ /**
3606
+ * Search contacts with advanced filters
3607
+ */
3608
+ async searchContacts(query, filters) {
3609
+ const queryParams = new URLSearchParams();
3610
+ queryParams.set('search', query);
3611
+ if (filters?.status)
3612
+ queryParams.set('status', filters.status);
3613
+ if (filters?.lifecycleStage)
3614
+ queryParams.set('lifecycleStage', filters.lifecycleStage);
3615
+ if (filters?.source)
3616
+ queryParams.set('source', filters.source);
3617
+ if (filters?.tags)
3618
+ queryParams.set('tags', filters.tags.join(','));
3619
+ if (filters?.page)
3620
+ queryParams.set('page', filters.page.toString());
3621
+ if (filters?.limit)
3622
+ queryParams.set('limit', filters.limit.toString());
3623
+ const qs = queryParams.toString();
3624
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts${qs ? `?${qs}` : ''}`;
3387
3625
  return this.request(endpoint);
3388
3626
  }
3389
3627
  // ============================================
3390
- // PIPELINES API
3628
+ // WEBHOOK MANAGEMENT API
3391
3629
  // ============================================
3392
3630
  /**
3393
- * Get all pipelines
3631
+ * List all webhook subscriptions
3394
3632
  */
3395
- async getPipelines() {
3396
- return this.request(`/api/workspaces/${this.workspaceId}/pipelines`);
3633
+ async listWebhooks(params) {
3634
+ const queryParams = new URLSearchParams();
3635
+ if (params?.page)
3636
+ queryParams.set('page', params.page.toString());
3637
+ if (params?.limit)
3638
+ queryParams.set('limit', params.limit.toString());
3639
+ const query = queryParams.toString();
3640
+ return this.request(`/api/workspaces/${this.workspaceId}/webhooks${query ? `?${query}` : ''}`);
3397
3641
  }
3398
3642
  /**
3399
- * Get a single pipeline by ID
3643
+ * Create a new webhook subscription
3400
3644
  */
3401
- async getPipeline(pipelineId) {
3402
- return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`);
3645
+ async createWebhook(data) {
3646
+ return this.request(`/api/workspaces/${this.workspaceId}/webhooks`, {
3647
+ method: 'POST',
3648
+ body: JSON.stringify(data),
3649
+ });
3403
3650
  }
3404
3651
  /**
3405
- * Create a new pipeline
3652
+ * Delete a webhook subscription
3406
3653
  */
3407
- async createPipeline(pipeline) {
3408
- return this.request(`/api/workspaces/${this.workspaceId}/pipelines`, {
3409
- method: 'POST',
3410
- body: JSON.stringify(pipeline),
3654
+ async deleteWebhook(webhookId) {
3655
+ this.validateRequired('webhookId', webhookId, 'deleteWebhook');
3656
+ return this.request(`/api/workspaces/${this.workspaceId}/webhooks/${webhookId}`, {
3657
+ method: 'DELETE',
3411
3658
  });
3412
3659
  }
3660
+ // ============================================
3661
+ // EVENT TRIGGERS API (delegated to triggers manager)
3662
+ // ============================================
3663
+ /**
3664
+ * Get all event triggers
3665
+ */
3666
+ async getEventTriggers() {
3667
+ return this.triggers.getTriggers();
3668
+ }
3669
+ /**
3670
+ * Create a new event trigger
3671
+ */
3672
+ async createEventTrigger(trigger) {
3673
+ return this.triggers.createTrigger(trigger);
3674
+ }
3413
3675
  /**
3414
- * Update an existing pipeline
3676
+ * Update an event trigger
3415
3677
  */
3416
- async updatePipeline(pipelineId, updates) {
3417
- return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
3418
- method: 'PUT',
3419
- body: JSON.stringify(updates),
3420
- });
3678
+ async updateEventTrigger(triggerId, updates) {
3679
+ return this.triggers.updateTrigger(triggerId, updates);
3421
3680
  }
3422
3681
  /**
3423
- * Delete a pipeline
3682
+ * Delete an event trigger
3424
3683
  */
3425
- async deletePipeline(pipelineId) {
3426
- return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
3427
- method: 'DELETE',
3684
+ async deleteEventTrigger(triggerId) {
3685
+ return this.triggers.deleteTrigger(triggerId);
3686
+ }
3687
+ }
3688
+
3689
+ /**
3690
+ * Clianta SDK - Main Tracker Class
3691
+ * @see SDK_VERSION in core/config.ts
3692
+ */
3693
+ /**
3694
+ * Main Clianta Tracker Class
3695
+ */
3696
+ class Tracker {
3697
+ constructor(workspaceId, userConfig = {}) {
3698
+ this.plugins = [];
3699
+ this.isInitialized = false;
3700
+ /** contactId after a successful identify() call */
3701
+ this.contactId = null;
3702
+ /** Pending identify retry on next flush */
3703
+ this.pendingIdentify = null;
3704
+ /** Registered event schemas for validation */
3705
+ this.eventSchemas = new Map();
3706
+ if (!workspaceId) {
3707
+ throw new Error('[Clianta] Workspace ID is required');
3708
+ }
3709
+ this.workspaceId = workspaceId;
3710
+ this.config = mergeConfig(userConfig);
3711
+ // Setup debug mode
3712
+ logger.enabled = this.config.debug;
3713
+ logger.info(`Initializing SDK v${SDK_VERSION}`, { workspaceId });
3714
+ // Initialize consent manager
3715
+ this.consentManager = new ConsentManager({
3716
+ ...this.config.consent,
3717
+ onConsentChange: (state, previous) => {
3718
+ this.onConsentChange(state, previous);
3719
+ },
3720
+ });
3721
+ // Initialize transport and queue
3722
+ this.transport = new Transport({ apiEndpoint: this.config.apiEndpoint });
3723
+ this.queue = new EventQueue(this.transport, {
3724
+ batchSize: this.config.batchSize,
3725
+ flushInterval: this.config.flushInterval,
3428
3726
  });
3727
+ // Get or create visitor and session IDs based on mode
3728
+ this.visitorId = this.createVisitorId();
3729
+ this.sessionId = this.createSessionId();
3730
+ logger.debug('IDs created', { visitorId: this.visitorId, sessionId: this.sessionId });
3731
+ // Security warnings
3732
+ if (this.config.apiEndpoint.startsWith('http://') &&
3733
+ typeof window !== 'undefined' &&
3734
+ !window.location.hostname.includes('localhost') &&
3735
+ !window.location.hostname.includes('127.0.0.1')) {
3736
+ logger.warn('apiEndpoint uses HTTP — events and visitor data will be sent unencrypted. Use HTTPS in production.');
3737
+ }
3738
+ if (this.config.apiKey && typeof window !== 'undefined') {
3739
+ logger.warn('API key is exposed in client-side code. Use API keys only in server-side (Node.js) environments.');
3740
+ }
3741
+ // Initialize plugins
3742
+ this.initPlugins();
3743
+ this.isInitialized = true;
3744
+ logger.info('SDK initialized successfully');
3429
3745
  }
3430
- // ============================================
3431
- // TASKS API
3432
- // ============================================
3433
3746
  /**
3434
- * Get all tasks with pagination
3747
+ * Create visitor ID based on storage mode
3435
3748
  */
3436
- async getTasks(params) {
3437
- const queryParams = new URLSearchParams();
3438
- if (params?.page)
3439
- queryParams.set('page', params.page.toString());
3440
- if (params?.limit)
3441
- queryParams.set('limit', params.limit.toString());
3442
- if (params?.status)
3443
- queryParams.set('status', params.status);
3444
- if (params?.priority)
3445
- queryParams.set('priority', params.priority);
3446
- if (params?.contactId)
3447
- queryParams.set('contactId', params.contactId);
3448
- if (params?.companyId)
3449
- queryParams.set('companyId', params.companyId);
3450
- if (params?.opportunityId)
3451
- queryParams.set('opportunityId', params.opportunityId);
3452
- const query = queryParams.toString();
3453
- const endpoint = `/api/workspaces/${this.workspaceId}/tasks${query ? `?${query}` : ''}`;
3454
- return this.request(endpoint);
3749
+ createVisitorId() {
3750
+ // Anonymous mode: use temporary ID until consent
3751
+ if (this.config.consent.anonymousMode && !this.consentManager.hasExplicit()) {
3752
+ const key = STORAGE_KEYS.VISITOR_ID + '_anon';
3753
+ let anonId = getSessionStorage(key);
3754
+ if (!anonId) {
3755
+ anonId = 'anon_' + generateUUID();
3756
+ setSessionStorage(key, anonId);
3757
+ }
3758
+ return anonId;
3759
+ }
3760
+ // Cookie-less mode: use sessionStorage only
3761
+ if (this.config.cookielessMode) {
3762
+ let visitorId = getSessionStorage(STORAGE_KEYS.VISITOR_ID);
3763
+ if (!visitorId) {
3764
+ visitorId = generateUUID();
3765
+ setSessionStorage(STORAGE_KEYS.VISITOR_ID, visitorId);
3766
+ }
3767
+ return visitorId;
3768
+ }
3769
+ // Normal mode
3770
+ return getOrCreateVisitorId(this.config.useCookies);
3455
3771
  }
3456
3772
  /**
3457
- * Get a single task by ID
3773
+ * Create session ID
3458
3774
  */
3459
- async getTask(taskId) {
3460
- return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`);
3775
+ createSessionId() {
3776
+ return getOrCreateSessionId(this.config.sessionTimeout);
3461
3777
  }
3462
3778
  /**
3463
- * Create a new task
3779
+ * Handle consent state changes
3464
3780
  */
3465
- async createTask(task) {
3466
- return this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
3467
- method: 'POST',
3468
- body: JSON.stringify(task),
3469
- });
3781
+ onConsentChange(state, previous) {
3782
+ logger.debug('Consent changed:', { from: previous, to: state });
3783
+ // If analytics consent was just granted
3784
+ if (state.analytics && !previous.analytics) {
3785
+ // Upgrade from anonymous ID to persistent ID
3786
+ if (this.config.consent.anonymousMode) {
3787
+ this.visitorId = getOrCreateVisitorId(this.config.useCookies);
3788
+ logger.info('Upgraded from anonymous to persistent visitor ID');
3789
+ }
3790
+ // Flush buffered events
3791
+ const buffered = this.consentManager.flushBuffer();
3792
+ for (const event of buffered) {
3793
+ // Update event with new visitor ID
3794
+ event.visitorId = this.visitorId;
3795
+ this.queue.push(event);
3796
+ }
3797
+ }
3470
3798
  }
3471
3799
  /**
3472
- * Update an existing task
3800
+ * Initialize enabled plugins
3801
+ * Handles both sync and async plugin init methods
3473
3802
  */
3474
- async updateTask(taskId, updates) {
3475
- return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
3476
- method: 'PUT',
3477
- body: JSON.stringify(updates),
3478
- });
3803
+ initPlugins() {
3804
+ const pluginsToLoad = this.config.plugins;
3805
+ // Skip pageView plugin if autoPageView is disabled
3806
+ const filteredPlugins = this.config.autoPageView
3807
+ ? pluginsToLoad
3808
+ : pluginsToLoad.filter((p) => p !== 'pageView');
3809
+ for (const pluginName of filteredPlugins) {
3810
+ try {
3811
+ const plugin = getPlugin(pluginName);
3812
+ // Handle both sync and async init (fire-and-forget for async)
3813
+ const result = plugin.init(this);
3814
+ if (result instanceof Promise) {
3815
+ result.catch((error) => {
3816
+ logger.error(`Async plugin init failed: ${pluginName}`, error);
3817
+ });
3818
+ }
3819
+ this.plugins.push(plugin);
3820
+ logger.debug(`Plugin loaded: ${pluginName}`);
3821
+ }
3822
+ catch (error) {
3823
+ logger.error(`Failed to load plugin: ${pluginName}`, error);
3824
+ }
3825
+ }
3479
3826
  }
3480
3827
  /**
3481
- * Mark a task as completed
3828
+ * Track a custom event
3482
3829
  */
3483
- async completeTask(taskId) {
3484
- return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}/complete`, {
3485
- method: 'PATCH',
3486
- });
3830
+ track(eventType, eventName, properties = {}) {
3831
+ if (!this.isInitialized) {
3832
+ logger.warn('SDK not initialized, event dropped');
3833
+ return;
3834
+ }
3835
+ const event = {
3836
+ workspaceId: this.workspaceId,
3837
+ visitorId: this.visitorId,
3838
+ sessionId: this.sessionId,
3839
+ eventType: eventType,
3840
+ eventName,
3841
+ url: typeof window !== 'undefined' ? window.location.href : '',
3842
+ referrer: typeof document !== 'undefined' ? document.referrer || undefined : undefined,
3843
+ properties: {
3844
+ ...properties,
3845
+ eventId: generateUUID(), // Unique ID for deduplication on retry
3846
+ websiteDomain: typeof window !== 'undefined' ? window.location.hostname : undefined,
3847
+ },
3848
+ device: getDeviceInfo(),
3849
+ ...getUTMParams(),
3850
+ timestamp: new Date().toISOString(),
3851
+ sdkVersion: SDK_VERSION,
3852
+ };
3853
+ // Attach contactId if known (from a prior identify() call)
3854
+ if (this.contactId) {
3855
+ event.contactId = this.contactId;
3856
+ }
3857
+ // Validate event against registered schema (debug mode only)
3858
+ this.validateEventSchema(eventType, properties);
3859
+ // Check consent before tracking
3860
+ if (!this.consentManager.canTrack()) {
3861
+ // Buffer event for later if waitForConsent is enabled
3862
+ if (this.config.consent.waitForConsent) {
3863
+ this.consentManager.bufferEvent(event);
3864
+ return;
3865
+ }
3866
+ // Otherwise drop the event
3867
+ logger.debug('Event dropped (no consent):', eventName);
3868
+ return;
3869
+ }
3870
+ this.queue.push(event);
3871
+ logger.debug('Event tracked:', eventName, properties);
3487
3872
  }
3488
3873
  /**
3489
- * Delete a task
3874
+ * Track a page view
3490
3875
  */
3491
- async deleteTask(taskId) {
3492
- return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
3493
- method: 'DELETE',
3876
+ page(name, properties = {}) {
3877
+ const pageName = name || (typeof document !== 'undefined' ? document.title : 'Page View');
3878
+ this.track('page_view', pageName, {
3879
+ ...properties,
3880
+ path: typeof window !== 'undefined' ? window.location.pathname : '',
3494
3881
  });
3495
3882
  }
3496
- // ============================================
3497
- // ACTIVITIES API
3498
- // ============================================
3499
3883
  /**
3500
- * Get activities for a contact
3884
+ * Identify a visitor.
3885
+ * Links the anonymous visitorId to a CRM contact and returns the contactId.
3886
+ * All subsequent track() calls will include the contactId automatically.
3501
3887
  */
3502
- async getContactActivities(contactId, params) {
3503
- const queryParams = new URLSearchParams();
3504
- if (params?.page)
3505
- queryParams.set('page', params.page.toString());
3506
- if (params?.limit)
3507
- queryParams.set('limit', params.limit.toString());
3508
- if (params?.type)
3509
- queryParams.set('type', params.type);
3510
- const query = queryParams.toString();
3511
- const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
3512
- return this.request(endpoint);
3888
+ async identify(email, traits = {}) {
3889
+ if (!email) {
3890
+ logger.warn('Email is required for identification');
3891
+ return null;
3892
+ }
3893
+ if (!isValidEmail(email)) {
3894
+ logger.warn('Invalid email format, identification skipped:', email);
3895
+ return null;
3896
+ }
3897
+ logger.info('Identifying visitor:', email);
3898
+ const result = await this.transport.sendIdentify({
3899
+ workspaceId: this.workspaceId,
3900
+ visitorId: this.visitorId,
3901
+ email,
3902
+ properties: traits,
3903
+ });
3904
+ if (result.success) {
3905
+ logger.info('Visitor identified successfully, contactId:', result.contactId);
3906
+ // Store contactId so all future track() calls include it
3907
+ this.contactId = result.contactId ?? null;
3908
+ this.pendingIdentify = null;
3909
+ return this.contactId;
3910
+ }
3911
+ else {
3912
+ logger.error('Failed to identify visitor:', result.error);
3913
+ // Store for retry on next flush
3914
+ this.pendingIdentify = { email, traits };
3915
+ return null;
3916
+ }
3513
3917
  }
3514
3918
  /**
3515
- * Get activities for an opportunity/deal
3919
+ * Send a server-side inbound event via the API key endpoint.
3920
+ * Convenience proxy to CRMClient.sendEvent() — requires apiKey in config.
3516
3921
  */
3517
- async getOpportunityActivities(opportunityId, params) {
3518
- const queryParams = new URLSearchParams();
3519
- if (params?.page)
3520
- queryParams.set('page', params.page.toString());
3521
- if (params?.limit)
3522
- queryParams.set('limit', params.limit.toString());
3523
- if (params?.type)
3524
- queryParams.set('type', params.type);
3525
- const query = queryParams.toString();
3526
- const endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/activities${query ? `?${query}` : ''}`;
3527
- return this.request(endpoint);
3922
+ async sendEvent(payload) {
3923
+ const apiKey = this.config.apiKey;
3924
+ if (!apiKey) {
3925
+ logger.error('sendEvent() requires an apiKey in the SDK config');
3926
+ return { success: false, contactCreated: false, event: payload.event, error: 'No API key configured' };
3927
+ }
3928
+ const client = new CRMClient(this.config.apiEndpoint, this.workspaceId, undefined, apiKey);
3929
+ return client.sendEvent(payload);
3528
3930
  }
3529
3931
  /**
3530
- * Create a new activity
3932
+ * Get the current visitor's profile from the CRM.
3933
+ * Returns visitor data and linked contact info if identified.
3934
+ * Only returns data for the current visitor (privacy-safe for frontend).
3531
3935
  */
3532
- async createActivity(activity) {
3533
- // Determine the correct endpoint based on related entity
3534
- let endpoint;
3535
- if (activity.opportunityId) {
3536
- endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${activity.opportunityId}/activities`;
3936
+ async getVisitorProfile() {
3937
+ if (!this.isInitialized) {
3938
+ logger.warn('SDK not initialized');
3939
+ return null;
3537
3940
  }
3538
- else if (activity.contactId) {
3539
- endpoint = `/api/workspaces/${this.workspaceId}/contacts/${activity.contactId}/activities`;
3941
+ const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/profile`);
3942
+ if (result.success && result.data) {
3943
+ logger.debug('Visitor profile fetched:', result.data);
3944
+ return result.data;
3540
3945
  }
3541
- else {
3542
- endpoint = `/api/workspaces/${this.workspaceId}/activities`;
3946
+ logger.warn('Failed to fetch visitor profile:', result.error);
3947
+ return null;
3948
+ }
3949
+ /**
3950
+ * Get the current visitor's recent activity/events.
3951
+ * Returns paginated list of tracking events for this visitor.
3952
+ */
3953
+ async getVisitorActivity(options) {
3954
+ if (!this.isInitialized) {
3955
+ logger.warn('SDK not initialized');
3956
+ return null;
3543
3957
  }
3544
- return this.request(endpoint, {
3545
- method: 'POST',
3546
- body: JSON.stringify(activity),
3547
- });
3958
+ const params = {};
3959
+ if (options?.page)
3960
+ params.page = options.page.toString();
3961
+ if (options?.limit)
3962
+ params.limit = options.limit.toString();
3963
+ if (options?.eventType)
3964
+ params.eventType = options.eventType;
3965
+ if (options?.startDate)
3966
+ params.startDate = options.startDate;
3967
+ if (options?.endDate)
3968
+ params.endDate = options.endDate;
3969
+ const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/activity`, params);
3970
+ if (result.success && result.data) {
3971
+ return result.data;
3972
+ }
3973
+ logger.warn('Failed to fetch visitor activity:', result.error);
3974
+ return null;
3548
3975
  }
3549
3976
  /**
3550
- * Update an existing activity
3977
+ * Get a summarized journey timeline for the current visitor.
3978
+ * Includes top pages, sessions, time spent, and recent activities.
3551
3979
  */
3552
- async updateActivity(activityId, updates) {
3553
- return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
3554
- method: 'PATCH',
3555
- body: JSON.stringify(updates),
3556
- });
3980
+ async getVisitorTimeline() {
3981
+ if (!this.isInitialized) {
3982
+ logger.warn('SDK not initialized');
3983
+ return null;
3984
+ }
3985
+ const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/timeline`);
3986
+ if (result.success && result.data) {
3987
+ return result.data;
3988
+ }
3989
+ logger.warn('Failed to fetch visitor timeline:', result.error);
3990
+ return null;
3557
3991
  }
3558
3992
  /**
3559
- * Delete an activity
3993
+ * Get engagement metrics for the current visitor.
3994
+ * Includes time on site, page views, bounce rate, and engagement score.
3560
3995
  */
3561
- async deleteActivity(activityId) {
3562
- return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
3563
- method: 'DELETE',
3564
- });
3996
+ async getVisitorEngagement() {
3997
+ if (!this.isInitialized) {
3998
+ logger.warn('SDK not initialized');
3999
+ return null;
4000
+ }
4001
+ const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/engagement`);
4002
+ if (result.success && result.data) {
4003
+ return result.data;
4004
+ }
4005
+ logger.warn('Failed to fetch visitor engagement:', result.error);
4006
+ return null;
3565
4007
  }
3566
4008
  /**
3567
- * Log a call activity
4009
+ * Retry pending identify call
3568
4010
  */
3569
- async logCall(data) {
3570
- return this.createActivity({
3571
- type: 'call',
3572
- title: `${data.direction === 'inbound' ? 'Inbound' : 'Outbound'} Call`,
3573
- direction: data.direction,
3574
- duration: data.duration,
3575
- outcome: data.outcome,
3576
- description: data.notes,
3577
- contactId: data.contactId,
3578
- opportunityId: data.opportunityId,
3579
- });
4011
+ async retryPendingIdentify() {
4012
+ if (!this.pendingIdentify)
4013
+ return;
4014
+ const { email, traits } = this.pendingIdentify;
4015
+ this.pendingIdentify = null;
4016
+ await this.identify(email, traits);
3580
4017
  }
3581
4018
  /**
3582
- * Log a meeting activity
4019
+ * Update consent state
3583
4020
  */
3584
- async logMeeting(data) {
3585
- return this.createActivity({
3586
- type: 'meeting',
3587
- title: data.title,
3588
- duration: data.duration,
3589
- outcome: data.outcome,
3590
- description: data.notes,
3591
- contactId: data.contactId,
3592
- opportunityId: data.opportunityId,
3593
- });
4021
+ consent(state) {
4022
+ this.consentManager.update(state);
3594
4023
  }
3595
4024
  /**
3596
- * Add a note to a contact or opportunity
4025
+ * Get current consent state
3597
4026
  */
3598
- async addNote(data) {
3599
- return this.createActivity({
3600
- type: 'note',
3601
- title: 'Note',
3602
- description: data.content,
3603
- contactId: data.contactId,
3604
- opportunityId: data.opportunityId,
3605
- });
4027
+ getConsentState() {
4028
+ return this.consentManager.getState();
3606
4029
  }
3607
- // ============================================
3608
- // EMAIL TEMPLATES API
3609
- // ============================================
3610
4030
  /**
3611
- * Get all email templates
4031
+ * Toggle debug mode
3612
4032
  */
3613
- async getEmailTemplates(params) {
3614
- const queryParams = new URLSearchParams();
3615
- if (params?.page)
3616
- queryParams.set('page', params.page.toString());
3617
- if (params?.limit)
3618
- queryParams.set('limit', params.limit.toString());
3619
- const query = queryParams.toString();
3620
- const endpoint = `/api/workspaces/${this.workspaceId}/email-templates${query ? `?${query}` : ''}`;
3621
- return this.request(endpoint);
4033
+ debug(enabled) {
4034
+ logger.enabled = enabled;
4035
+ logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
3622
4036
  }
3623
4037
  /**
3624
- * Get a single email template by ID
4038
+ * Register a schema for event validation.
4039
+ * When debug mode is enabled, events will be validated against registered schemas.
4040
+ *
4041
+ * @example
4042
+ * tracker.registerEventSchema('purchase', {
4043
+ * productId: 'string',
4044
+ * price: 'number',
4045
+ * quantity: 'number',
4046
+ * });
3625
4047
  */
3626
- async getEmailTemplate(templateId) {
3627
- return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`);
4048
+ registerEventSchema(eventType, schema) {
4049
+ this.eventSchemas.set(eventType, schema);
4050
+ logger.debug('Event schema registered:', eventType);
3628
4051
  }
3629
4052
  /**
3630
- * Create a new email template
4053
+ * Validate event properties against a registered schema (debug mode only)
3631
4054
  */
3632
- async createEmailTemplate(template) {
3633
- return this.request(`/api/workspaces/${this.workspaceId}/email-templates`, {
3634
- method: 'POST',
3635
- body: JSON.stringify(template),
3636
- });
4055
+ validateEventSchema(eventType, properties) {
4056
+ if (!this.config.debug)
4057
+ return;
4058
+ const schema = this.eventSchemas.get(eventType);
4059
+ if (!schema)
4060
+ return;
4061
+ for (const [key, expectedType] of Object.entries(schema)) {
4062
+ const value = properties[key];
4063
+ if (value === undefined) {
4064
+ logger.warn(`[Schema] Missing property "${key}" for event type "${eventType}"`);
4065
+ continue;
4066
+ }
4067
+ let valid = false;
4068
+ switch (expectedType) {
4069
+ case 'string':
4070
+ valid = typeof value === 'string';
4071
+ break;
4072
+ case 'number':
4073
+ valid = typeof value === 'number';
4074
+ break;
4075
+ case 'boolean':
4076
+ valid = typeof value === 'boolean';
4077
+ break;
4078
+ case 'object':
4079
+ valid = typeof value === 'object' && !Array.isArray(value);
4080
+ break;
4081
+ case 'array':
4082
+ valid = Array.isArray(value);
4083
+ break;
4084
+ }
4085
+ if (!valid) {
4086
+ logger.warn(`[Schema] Property "${key}" for event "${eventType}" expected ${expectedType}, got ${typeof value}`);
4087
+ }
4088
+ }
3637
4089
  }
3638
4090
  /**
3639
- * Update an email template
4091
+ * Get visitor ID
3640
4092
  */
3641
- async updateEmailTemplate(templateId, updates) {
3642
- return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
3643
- method: 'PUT',
3644
- body: JSON.stringify(updates),
3645
- });
4093
+ getVisitorId() {
4094
+ return this.visitorId;
3646
4095
  }
3647
4096
  /**
3648
- * Delete an email template
4097
+ * Get session ID
3649
4098
  */
3650
- async deleteEmailTemplate(templateId) {
3651
- return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
3652
- method: 'DELETE',
3653
- });
4099
+ getSessionId() {
4100
+ return this.sessionId;
3654
4101
  }
3655
4102
  /**
3656
- * Send an email using a template
4103
+ * Get workspace ID
3657
4104
  */
3658
- async sendEmail(data) {
3659
- return this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
3660
- method: 'POST',
3661
- body: JSON.stringify(data),
3662
- });
4105
+ getWorkspaceId() {
4106
+ return this.workspaceId;
3663
4107
  }
3664
- // ============================================
3665
- // EVENT TRIGGERS API (delegated to triggers manager)
3666
- // ============================================
3667
4108
  /**
3668
- * Get all event triggers
4109
+ * Get current configuration
3669
4110
  */
3670
- async getEventTriggers() {
3671
- return this.triggers.getTriggers();
4111
+ getConfig() {
4112
+ return { ...this.config };
3672
4113
  }
3673
4114
  /**
3674
- * Create a new event trigger
4115
+ * Force flush event queue
3675
4116
  */
3676
- async createEventTrigger(trigger) {
3677
- return this.triggers.createTrigger(trigger);
4117
+ async flush() {
4118
+ await this.retryPendingIdentify();
4119
+ await this.queue.flush();
3678
4120
  }
3679
4121
  /**
3680
- * Update an event trigger
4122
+ * Reset visitor and session (for logout)
3681
4123
  */
3682
- async updateEventTrigger(triggerId, updates) {
3683
- return this.triggers.updateTrigger(triggerId, updates);
4124
+ reset() {
4125
+ logger.info('Resetting visitor data');
4126
+ resetIds(this.config.useCookies);
4127
+ this.visitorId = this.createVisitorId();
4128
+ this.sessionId = this.createSessionId();
4129
+ this.contactId = null;
4130
+ this.pendingIdentify = null;
4131
+ this.queue.clear();
3684
4132
  }
3685
4133
  /**
3686
- * Delete an event trigger
4134
+ * Delete all stored user data (GDPR right-to-erasure)
3687
4135
  */
3688
- async deleteEventTrigger(triggerId) {
3689
- return this.triggers.deleteTrigger(triggerId);
4136
+ deleteData() {
4137
+ logger.info('Deleting all user data (GDPR request)');
4138
+ // Clear queue
4139
+ this.queue.clear();
4140
+ // Reset consent
4141
+ this.consentManager.reset();
4142
+ // Clear all stored IDs
4143
+ resetIds(this.config.useCookies);
4144
+ // Clear session storage items
4145
+ if (typeof sessionStorage !== 'undefined') {
4146
+ try {
4147
+ sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
4148
+ sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID + '_anon');
4149
+ sessionStorage.removeItem(STORAGE_KEYS.SESSION_ID);
4150
+ sessionStorage.removeItem(STORAGE_KEYS.SESSION_TIMESTAMP);
4151
+ }
4152
+ catch {
4153
+ // Ignore errors
4154
+ }
4155
+ }
4156
+ // Clear localStorage items
4157
+ if (typeof localStorage !== 'undefined') {
4158
+ try {
4159
+ localStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
4160
+ localStorage.removeItem(STORAGE_KEYS.CONSENT);
4161
+ localStorage.removeItem(STORAGE_KEYS.EVENT_QUEUE);
4162
+ }
4163
+ catch {
4164
+ // Ignore errors
4165
+ }
4166
+ }
4167
+ // Generate new IDs
4168
+ this.visitorId = this.createVisitorId();
4169
+ this.sessionId = this.createSessionId();
4170
+ logger.info('All user data deleted');
4171
+ }
4172
+ /**
4173
+ * Destroy tracker and cleanup
4174
+ */
4175
+ async destroy() {
4176
+ logger.info('Destroying tracker');
4177
+ // Flush any remaining events (await to ensure completion)
4178
+ await this.queue.flush();
4179
+ // Destroy plugins
4180
+ for (const plugin of this.plugins) {
4181
+ if (plugin.destroy) {
4182
+ plugin.destroy();
4183
+ }
4184
+ }
4185
+ this.plugins = [];
4186
+ // Destroy queue
4187
+ this.queue.destroy();
4188
+ this.isInitialized = false;
3690
4189
  }
3691
4190
  }
3692
4191
 
@@ -3778,7 +4277,9 @@ const CliantaContext = createContext(null);
3778
4277
  * </CliantaProvider>
3779
4278
  */
3780
4279
  function CliantaProvider({ config, children }) {
3781
- const trackerRef = useRef(null);
4280
+ const [tracker, setTracker] = useState(null);
4281
+ // Stable ref to projectId — the only value that truly identifies the tracker
4282
+ const projectIdRef = useRef(config.projectId);
3782
4283
  useEffect(() => {
3783
4284
  // Initialize tracker with config
3784
4285
  const projectId = config.projectId;
@@ -3786,15 +4287,21 @@ function CliantaProvider({ config, children }) {
3786
4287
  console.error('[Clianta] Missing projectId in config. Please add projectId to your clianta.config.ts');
3787
4288
  return;
3788
4289
  }
4290
+ // Only re-initialize if projectId actually changed
4291
+ if (projectIdRef.current !== projectId) {
4292
+ projectIdRef.current = projectId;
4293
+ }
3789
4294
  // Extract projectId (handled separately) and pass rest as options
3790
4295
  const { projectId: _, ...options } = config;
3791
- trackerRef.current = clianta(projectId, options);
4296
+ const instance = clianta(projectId, options);
4297
+ setTracker(instance);
3792
4298
  // Cleanup: flush pending events on unmount
3793
4299
  return () => {
3794
- trackerRef.current?.flush();
4300
+ instance?.flush();
3795
4301
  };
3796
- }, [config]);
3797
- return (jsx(CliantaContext.Provider, { value: trackerRef.current, children: children }));
4302
+ // eslint-disable-next-line react-hooks/exhaustive-deps
4303
+ }, [config.projectId]);
4304
+ return (jsx(CliantaContext.Provider, { value: tracker, children: children }));
3798
4305
  }
3799
4306
  /**
3800
4307
  * useClianta - Hook to access tracker in any component