@clianta/sdk 1.1.1 → 1.3.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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Clianta SDK v1.1.1
2
+ * Clianta SDK v1.3.0
3
3
  * (c) 2026 Clianta
4
4
  * Released under the MIT License.
5
5
  */
@@ -14,7 +14,7 @@
14
14
  * @see SDK_VERSION in core/config.ts
15
15
  */
16
16
  /** SDK Version */
17
- const SDK_VERSION = '1.1.0';
17
+ const SDK_VERSION = '1.3.0';
18
18
  /** Default API endpoint based on environment */
19
19
  const getDefaultApiEndpoint = () => {
20
20
  if (typeof window === 'undefined')
@@ -34,7 +34,6 @@
34
34
  'engagement',
35
35
  'downloads',
36
36
  'exitIntent',
37
- 'popupForms',
38
37
  ];
39
38
  /** Default configuration values */
40
39
  const DEFAULT_CONFIG = {
@@ -578,14 +577,24 @@
578
577
  * @see SDK_VERSION in core/config.ts
579
578
  */
580
579
  const MAX_QUEUE_SIZE = 1000;
580
+ /** Rate limit: max events per window */
581
+ const RATE_LIMIT_MAX_EVENTS = 100;
582
+ /** Rate limit window in ms (1 minute) */
583
+ const RATE_LIMIT_WINDOW_MS = 60000;
581
584
  /**
582
- * Event queue with batching, persistence, and auto-flush
585
+ * Event queue with batching, persistence, rate limiting, and auto-flush
583
586
  */
584
587
  class EventQueue {
585
588
  constructor(transport, config = {}) {
586
589
  this.queue = [];
587
590
  this.flushTimer = null;
588
591
  this.isFlushing = false;
592
+ /** Rate limiting: timestamps of recent events */
593
+ this.eventTimestamps = [];
594
+ /** Unload handler references for cleanup */
595
+ this.boundBeforeUnload = null;
596
+ this.boundVisibilityChange = null;
597
+ this.boundPageHide = null;
589
598
  this.transport = transport;
590
599
  this.config = {
591
600
  batchSize: config.batchSize ?? 10,
@@ -604,6 +613,11 @@
604
613
  * Add an event to the queue
605
614
  */
606
615
  push(event) {
616
+ // Rate limiting check
617
+ if (!this.checkRateLimit()) {
618
+ logger.warn('Rate limit exceeded, event dropped:', event.eventName);
619
+ return;
620
+ }
607
621
  // Don't exceed max queue size
608
622
  if (this.queue.length >= this.config.maxQueueSize) {
609
623
  logger.warn('Queue full, dropping oldest event');
@@ -616,6 +630,22 @@
616
630
  this.flush();
617
631
  }
618
632
  }
633
+ /**
634
+ * Check and enforce rate limiting
635
+ * @returns true if event is allowed, false if rate limited
636
+ */
637
+ checkRateLimit() {
638
+ const now = Date.now();
639
+ // Remove timestamps outside the window
640
+ this.eventTimestamps = this.eventTimestamps.filter(ts => now - ts < RATE_LIMIT_WINDOW_MS);
641
+ // Check if under limit
642
+ if (this.eventTimestamps.length >= RATE_LIMIT_MAX_EVENTS) {
643
+ return false;
644
+ }
645
+ // Record this event
646
+ this.eventTimestamps.push(now);
647
+ return true;
648
+ }
619
649
  /**
620
650
  * Flush the queue (send all events)
621
651
  */
@@ -624,9 +654,10 @@
624
654
  return;
625
655
  }
626
656
  this.isFlushing = true;
657
+ // Atomically take snapshot of current queue length to avoid race condition
658
+ const count = this.queue.length;
659
+ const events = this.queue.splice(0, count);
627
660
  try {
628
- // Take all events from queue
629
- const events = this.queue.splice(0, this.queue.length);
630
661
  logger.debug(`Flushing ${events.length} events`);
631
662
  // Clear persisted queue
632
663
  this.persistQueue([]);
@@ -678,13 +709,25 @@
678
709
  this.persistQueue([]);
679
710
  }
680
711
  /**
681
- * Stop the flush timer
712
+ * Stop the flush timer and cleanup handlers
682
713
  */
683
714
  destroy() {
684
715
  if (this.flushTimer) {
685
716
  clearInterval(this.flushTimer);
686
717
  this.flushTimer = null;
687
718
  }
719
+ // Remove unload handlers
720
+ if (typeof window !== 'undefined') {
721
+ if (this.boundBeforeUnload) {
722
+ window.removeEventListener('beforeunload', this.boundBeforeUnload);
723
+ }
724
+ if (this.boundVisibilityChange) {
725
+ window.removeEventListener('visibilitychange', this.boundVisibilityChange);
726
+ }
727
+ if (this.boundPageHide) {
728
+ window.removeEventListener('pagehide', this.boundPageHide);
729
+ }
730
+ }
688
731
  }
689
732
  /**
690
733
  * Start auto-flush timer
@@ -704,19 +747,18 @@
704
747
  if (typeof window === 'undefined')
705
748
  return;
706
749
  // Flush on page unload
707
- window.addEventListener('beforeunload', () => {
708
- this.flushSync();
709
- });
750
+ this.boundBeforeUnload = () => this.flushSync();
751
+ window.addEventListener('beforeunload', this.boundBeforeUnload);
710
752
  // Flush when page becomes hidden
711
- window.addEventListener('visibilitychange', () => {
753
+ this.boundVisibilityChange = () => {
712
754
  if (document.visibilityState === 'hidden') {
713
755
  this.flushSync();
714
756
  }
715
- });
757
+ };
758
+ window.addEventListener('visibilitychange', this.boundVisibilityChange);
716
759
  // Flush on page hide (iOS Safari)
717
- window.addEventListener('pagehide', () => {
718
- this.flushSync();
719
- });
760
+ this.boundPageHide = () => this.flushSync();
761
+ window.addEventListener('pagehide', this.boundPageHide);
720
762
  }
721
763
  /**
722
764
  * Persist queue to localStorage
@@ -784,6 +826,9 @@
784
826
  constructor() {
785
827
  super(...arguments);
786
828
  this.name = 'pageView';
829
+ this.originalPushState = null;
830
+ this.originalReplaceState = null;
831
+ this.popstateHandler = null;
787
832
  }
788
833
  init(tracker) {
789
834
  super.init(tracker);
@@ -791,22 +836,40 @@
791
836
  this.trackPageView();
792
837
  // Track SPA navigation (History API)
793
838
  if (typeof window !== 'undefined') {
839
+ // Store originals for cleanup
840
+ this.originalPushState = history.pushState;
841
+ this.originalReplaceState = history.replaceState;
794
842
  // Intercept pushState and replaceState
795
- const originalPushState = history.pushState;
796
- const originalReplaceState = history.replaceState;
797
- history.pushState = (...args) => {
798
- originalPushState.apply(history, args);
799
- this.trackPageView();
843
+ const self = this;
844
+ history.pushState = function (...args) {
845
+ self.originalPushState.apply(history, args);
846
+ self.trackPageView();
800
847
  };
801
- history.replaceState = (...args) => {
802
- originalReplaceState.apply(history, args);
803
- this.trackPageView();
848
+ history.replaceState = function (...args) {
849
+ self.originalReplaceState.apply(history, args);
850
+ self.trackPageView();
804
851
  };
805
852
  // Handle back/forward navigation
806
- window.addEventListener('popstate', () => {
807
- this.trackPageView();
808
- });
853
+ this.popstateHandler = () => this.trackPageView();
854
+ window.addEventListener('popstate', this.popstateHandler);
855
+ }
856
+ }
857
+ destroy() {
858
+ // Restore original history methods
859
+ if (this.originalPushState) {
860
+ history.pushState = this.originalPushState;
861
+ this.originalPushState = null;
862
+ }
863
+ if (this.originalReplaceState) {
864
+ history.replaceState = this.originalReplaceState;
865
+ this.originalReplaceState = null;
809
866
  }
867
+ // Remove popstate listener
868
+ if (this.popstateHandler && typeof window !== 'undefined') {
869
+ window.removeEventListener('popstate', this.popstateHandler);
870
+ this.popstateHandler = null;
871
+ }
872
+ super.destroy();
810
873
  }
811
874
  trackPageView() {
812
875
  if (typeof window === 'undefined' || typeof document === 'undefined')
@@ -838,6 +901,10 @@
838
901
  this.pageLoadTime = 0;
839
902
  this.scrollTimeout = null;
840
903
  this.boundHandler = null;
904
+ /** SPA navigation support */
905
+ this.originalPushState = null;
906
+ this.originalReplaceState = null;
907
+ this.popstateHandler = null;
841
908
  }
842
909
  init(tracker) {
843
910
  super.init(tracker);
@@ -845,6 +912,8 @@
845
912
  if (typeof window !== 'undefined') {
846
913
  this.boundHandler = this.handleScroll.bind(this);
847
914
  window.addEventListener('scroll', this.boundHandler, { passive: true });
915
+ // Setup SPA navigation reset
916
+ this.setupNavigationReset();
848
917
  }
849
918
  }
850
919
  destroy() {
@@ -854,8 +923,53 @@
854
923
  if (this.scrollTimeout) {
855
924
  clearTimeout(this.scrollTimeout);
856
925
  }
926
+ // Restore original history methods
927
+ if (this.originalPushState) {
928
+ history.pushState = this.originalPushState;
929
+ this.originalPushState = null;
930
+ }
931
+ if (this.originalReplaceState) {
932
+ history.replaceState = this.originalReplaceState;
933
+ this.originalReplaceState = null;
934
+ }
935
+ // Remove popstate listener
936
+ if (this.popstateHandler && typeof window !== 'undefined') {
937
+ window.removeEventListener('popstate', this.popstateHandler);
938
+ this.popstateHandler = null;
939
+ }
857
940
  super.destroy();
858
941
  }
942
+ /**
943
+ * Reset scroll tracking for SPA navigation
944
+ */
945
+ resetForNavigation() {
946
+ this.milestonesReached.clear();
947
+ this.maxScrollDepth = 0;
948
+ this.pageLoadTime = Date.now();
949
+ }
950
+ /**
951
+ * Setup History API interception for SPA navigation
952
+ */
953
+ setupNavigationReset() {
954
+ if (typeof window === 'undefined')
955
+ return;
956
+ // Store originals for cleanup
957
+ this.originalPushState = history.pushState;
958
+ this.originalReplaceState = history.replaceState;
959
+ // Intercept pushState and replaceState
960
+ const self = this;
961
+ history.pushState = function (...args) {
962
+ self.originalPushState.apply(history, args);
963
+ self.resetForNavigation();
964
+ };
965
+ history.replaceState = function (...args) {
966
+ self.originalReplaceState.apply(history, args);
967
+ self.resetForNavigation();
968
+ };
969
+ // Handle back/forward navigation
970
+ this.popstateHandler = () => this.resetForNavigation();
971
+ window.addEventListener('popstate', this.popstateHandler);
972
+ }
859
973
  handleScroll() {
860
974
  // Debounce scroll tracking
861
975
  if (this.scrollTimeout) {
@@ -869,7 +983,11 @@
869
983
  const windowHeight = window.innerHeight;
870
984
  const documentHeight = document.documentElement.scrollHeight;
871
985
  const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
872
- const scrollPercent = Math.floor((scrollTop / (documentHeight - windowHeight)) * 100);
986
+ const scrollableHeight = documentHeight - windowHeight;
987
+ // Guard against divide-by-zero on short pages
988
+ if (scrollableHeight <= 0)
989
+ return;
990
+ const scrollPercent = Math.floor((scrollTop / scrollableHeight) * 100);
873
991
  // Clamp to valid range
874
992
  const clampedPercent = Math.max(0, Math.min(100, scrollPercent));
875
993
  // Update max scroll depth
@@ -904,6 +1022,7 @@
904
1022
  this.trackedForms = new WeakSet();
905
1023
  this.formInteractions = new Set();
906
1024
  this.observer = null;
1025
+ this.listeners = [];
907
1026
  }
908
1027
  init(tracker) {
909
1028
  super.init(tracker);
@@ -922,8 +1041,20 @@
922
1041
  this.observer.disconnect();
923
1042
  this.observer = null;
924
1043
  }
1044
+ // Remove all tracked event listeners
1045
+ for (const { element, event, handler } of this.listeners) {
1046
+ element.removeEventListener(event, handler);
1047
+ }
1048
+ this.listeners = [];
925
1049
  super.destroy();
926
1050
  }
1051
+ /**
1052
+ * Track event listener for cleanup
1053
+ */
1054
+ addListener(element, event, handler) {
1055
+ element.addEventListener(event, handler);
1056
+ this.listeners.push({ element, event, handler });
1057
+ }
927
1058
  trackAllForms() {
928
1059
  document.querySelectorAll('form').forEach((form) => {
929
1060
  this.setupFormTracking(form);
@@ -949,7 +1080,7 @@
949
1080
  if (!field.name || field.type === 'submit' || field.type === 'button')
950
1081
  return;
951
1082
  ['focus', 'blur', 'change'].forEach((eventType) => {
952
- field.addEventListener(eventType, () => {
1083
+ const handler = () => {
953
1084
  const key = `${formId}-${field.name}-${eventType}`;
954
1085
  if (!this.formInteractions.has(key)) {
955
1086
  this.formInteractions.add(key);
@@ -960,12 +1091,13 @@
960
1091
  interactionType: eventType,
961
1092
  });
962
1093
  }
963
- });
1094
+ };
1095
+ this.addListener(field, eventType, handler);
964
1096
  });
965
1097
  }
966
1098
  });
967
1099
  // Track form submission
968
- form.addEventListener('submit', () => {
1100
+ const submitHandler = () => {
969
1101
  this.track('form_submit', 'Form Submitted', {
970
1102
  formId,
971
1103
  action: form.action,
@@ -973,7 +1105,8 @@
973
1105
  });
974
1106
  // Auto-identify if email field found
975
1107
  this.autoIdentify(form);
976
- });
1108
+ };
1109
+ this.addListener(form, 'submit', submitHandler);
977
1110
  }
978
1111
  autoIdentify(form) {
979
1112
  const emailField = form.querySelector('input[type="email"], input[name*="email"]');
@@ -1057,6 +1190,7 @@
1057
1190
  this.engagementTimeout = null;
1058
1191
  this.boundMarkEngaged = null;
1059
1192
  this.boundTrackTimeOnPage = null;
1193
+ this.boundVisibilityHandler = null;
1060
1194
  }
1061
1195
  init(tracker) {
1062
1196
  super.init(tracker);
@@ -1067,12 +1201,7 @@
1067
1201
  // Setup engagement detection
1068
1202
  this.boundMarkEngaged = this.markEngaged.bind(this);
1069
1203
  this.boundTrackTimeOnPage = this.trackTimeOnPage.bind(this);
1070
- ['mousemove', 'keydown', 'touchstart', 'scroll'].forEach((event) => {
1071
- document.addEventListener(event, this.boundMarkEngaged, { passive: true });
1072
- });
1073
- // Track time on page before unload
1074
- window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
1075
- window.addEventListener('visibilitychange', () => {
1204
+ this.boundVisibilityHandler = () => {
1076
1205
  if (document.visibilityState === 'hidden') {
1077
1206
  this.trackTimeOnPage();
1078
1207
  }
@@ -1080,7 +1209,13 @@
1080
1209
  // Reset engagement timer when page becomes visible again
1081
1210
  this.engagementStartTime = Date.now();
1082
1211
  }
1212
+ };
1213
+ ['mousemove', 'keydown', 'touchstart', 'scroll'].forEach((event) => {
1214
+ document.addEventListener(event, this.boundMarkEngaged, { passive: true });
1083
1215
  });
1216
+ // Track time on page before unload
1217
+ window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
1218
+ document.addEventListener('visibilitychange', this.boundVisibilityHandler);
1084
1219
  }
1085
1220
  destroy() {
1086
1221
  if (this.boundMarkEngaged && typeof document !== 'undefined') {
@@ -1091,6 +1226,9 @@
1091
1226
  if (this.boundTrackTimeOnPage && typeof window !== 'undefined') {
1092
1227
  window.removeEventListener('beforeunload', this.boundTrackTimeOnPage);
1093
1228
  }
1229
+ if (this.boundVisibilityHandler && typeof document !== 'undefined') {
1230
+ document.removeEventListener('visibilitychange', this.boundVisibilityHandler);
1231
+ }
1094
1232
  if (this.engagementTimeout) {
1095
1233
  clearTimeout(this.engagementTimeout);
1096
1234
  }
@@ -1135,20 +1273,69 @@
1135
1273
  this.name = 'downloads';
1136
1274
  this.trackedDownloads = new Set();
1137
1275
  this.boundHandler = null;
1276
+ /** SPA navigation support */
1277
+ this.originalPushState = null;
1278
+ this.originalReplaceState = null;
1279
+ this.popstateHandler = null;
1138
1280
  }
1139
1281
  init(tracker) {
1140
1282
  super.init(tracker);
1141
1283
  if (typeof document !== 'undefined') {
1142
1284
  this.boundHandler = this.handleClick.bind(this);
1143
1285
  document.addEventListener('click', this.boundHandler, true);
1286
+ // Setup SPA navigation reset
1287
+ this.setupNavigationReset();
1144
1288
  }
1145
1289
  }
1146
1290
  destroy() {
1147
1291
  if (this.boundHandler && typeof document !== 'undefined') {
1148
1292
  document.removeEventListener('click', this.boundHandler, true);
1149
1293
  }
1294
+ // Restore original history methods
1295
+ if (this.originalPushState) {
1296
+ history.pushState = this.originalPushState;
1297
+ this.originalPushState = null;
1298
+ }
1299
+ if (this.originalReplaceState) {
1300
+ history.replaceState = this.originalReplaceState;
1301
+ this.originalReplaceState = null;
1302
+ }
1303
+ // Remove popstate listener
1304
+ if (this.popstateHandler && typeof window !== 'undefined') {
1305
+ window.removeEventListener('popstate', this.popstateHandler);
1306
+ this.popstateHandler = null;
1307
+ }
1150
1308
  super.destroy();
1151
1309
  }
1310
+ /**
1311
+ * Reset download tracking for SPA navigation
1312
+ */
1313
+ resetForNavigation() {
1314
+ this.trackedDownloads.clear();
1315
+ }
1316
+ /**
1317
+ * Setup History API interception for SPA navigation
1318
+ */
1319
+ setupNavigationReset() {
1320
+ if (typeof window === 'undefined')
1321
+ return;
1322
+ // Store originals for cleanup
1323
+ this.originalPushState = history.pushState;
1324
+ this.originalReplaceState = history.replaceState;
1325
+ // Intercept pushState and replaceState
1326
+ const self = this;
1327
+ history.pushState = function (...args) {
1328
+ self.originalPushState.apply(history, args);
1329
+ self.resetForNavigation();
1330
+ };
1331
+ history.replaceState = function (...args) {
1332
+ self.originalReplaceState.apply(history, args);
1333
+ self.resetForNavigation();
1334
+ };
1335
+ // Handle back/forward navigation
1336
+ this.popstateHandler = () => this.resetForNavigation();
1337
+ window.addEventListener('popstate', this.popstateHandler);
1338
+ }
1152
1339
  handleClick(e) {
1153
1340
  const link = e.target.closest('a');
1154
1341
  if (!link || !link.href)
@@ -1274,34 +1461,72 @@
1274
1461
  constructor() {
1275
1462
  super(...arguments);
1276
1463
  this.name = 'performance';
1464
+ this.boundLoadHandler = null;
1465
+ this.observers = [];
1466
+ this.boundClsVisibilityHandler = null;
1277
1467
  }
1278
1468
  init(tracker) {
1279
1469
  super.init(tracker);
1280
1470
  if (typeof window !== 'undefined') {
1281
1471
  // Track performance after page load
1282
- window.addEventListener('load', () => {
1472
+ this.boundLoadHandler = () => {
1283
1473
  // Delay to ensure all metrics are available
1284
1474
  setTimeout(() => this.trackPerformance(), 100);
1285
- });
1475
+ };
1476
+ window.addEventListener('load', this.boundLoadHandler);
1477
+ }
1478
+ }
1479
+ destroy() {
1480
+ if (this.boundLoadHandler && typeof window !== 'undefined') {
1481
+ window.removeEventListener('load', this.boundLoadHandler);
1286
1482
  }
1483
+ for (const observer of this.observers) {
1484
+ observer.disconnect();
1485
+ }
1486
+ this.observers = [];
1487
+ if (this.boundClsVisibilityHandler && typeof window !== 'undefined') {
1488
+ window.removeEventListener('visibilitychange', this.boundClsVisibilityHandler);
1489
+ }
1490
+ super.destroy();
1287
1491
  }
1288
1492
  trackPerformance() {
1289
1493
  if (typeof performance === 'undefined')
1290
1494
  return;
1291
- // Use Navigation Timing API
1292
- const timing = performance.timing;
1293
- if (!timing)
1294
- return;
1295
- const loadTime = timing.loadEventEnd - timing.navigationStart;
1296
- const domReady = timing.domContentLoadedEventEnd - timing.navigationStart;
1297
- const ttfb = timing.responseStart - timing.navigationStart;
1298
- const domInteractive = timing.domInteractive - timing.navigationStart;
1299
- this.track('performance', 'Page Performance', {
1300
- loadTime,
1301
- domReady,
1302
- ttfb, // Time to First Byte
1303
- domInteractive,
1304
- });
1495
+ // Use modern Navigation Timing API (PerformanceNavigationTiming)
1496
+ const entries = performance.getEntriesByType('navigation');
1497
+ if (entries.length > 0) {
1498
+ const navTiming = entries[0];
1499
+ const loadTime = Math.round(navTiming.loadEventEnd - navTiming.startTime);
1500
+ const domReady = Math.round(navTiming.domContentLoadedEventEnd - navTiming.startTime);
1501
+ const ttfb = Math.round(navTiming.responseStart - navTiming.requestStart);
1502
+ const domInteractive = Math.round(navTiming.domInteractive - navTiming.startTime);
1503
+ this.track('performance', 'Page Performance', {
1504
+ loadTime,
1505
+ domReady,
1506
+ ttfb, // Time to First Byte
1507
+ domInteractive,
1508
+ // Additional modern metrics
1509
+ dns: Math.round(navTiming.domainLookupEnd - navTiming.domainLookupStart),
1510
+ connection: Math.round(navTiming.connectEnd - navTiming.connectStart),
1511
+ transferSize: navTiming.transferSize,
1512
+ });
1513
+ }
1514
+ else {
1515
+ // Fallback for older browsers using deprecated API
1516
+ const timing = performance.timing;
1517
+ if (!timing)
1518
+ return;
1519
+ const loadTime = timing.loadEventEnd - timing.navigationStart;
1520
+ const domReady = timing.domContentLoadedEventEnd - timing.navigationStart;
1521
+ const ttfb = timing.responseStart - timing.navigationStart;
1522
+ const domInteractive = timing.domInteractive - timing.navigationStart;
1523
+ this.track('performance', 'Page Performance', {
1524
+ loadTime,
1525
+ domReady,
1526
+ ttfb,
1527
+ domInteractive,
1528
+ });
1529
+ }
1305
1530
  // Track Web Vitals if available
1306
1531
  this.trackWebVitals();
1307
1532
  }
@@ -1320,6 +1545,7 @@
1320
1545
  }
1321
1546
  });
1322
1547
  lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
1548
+ this.observers.push(lcpObserver);
1323
1549
  }
1324
1550
  catch {
1325
1551
  // LCP not supported
@@ -1337,6 +1563,7 @@
1337
1563
  }
1338
1564
  });
1339
1565
  fidObserver.observe({ type: 'first-input', buffered: true });
1566
+ this.observers.push(fidObserver);
1340
1567
  }
1341
1568
  catch {
1342
1569
  // FID not supported
@@ -1353,15 +1580,17 @@
1353
1580
  });
1354
1581
  });
1355
1582
  clsObserver.observe({ type: 'layout-shift', buffered: true });
1583
+ this.observers.push(clsObserver);
1356
1584
  // Report CLS after page is hidden
1357
- window.addEventListener('visibilitychange', () => {
1585
+ this.boundClsVisibilityHandler = () => {
1358
1586
  if (document.visibilityState === 'hidden' && clsValue > 0) {
1359
1587
  this.track('performance', 'Web Vital - CLS', {
1360
1588
  metric: 'CLS',
1361
1589
  value: Math.round(clsValue * 1000) / 1000,
1362
1590
  });
1363
1591
  }
1364
- }, { once: true });
1592
+ };
1593
+ window.addEventListener('visibilitychange', this.boundClsVisibilityHandler, { once: true });
1365
1594
  }
1366
1595
  catch {
1367
1596
  // CLS not supported
@@ -1578,8 +1807,8 @@
1578
1807
  opacity: 0;
1579
1808
  transition: all 0.3s ease;
1580
1809
  `;
1581
- // Build form HTML
1582
- container.innerHTML = this.buildFormHTML(form);
1810
+ // Build form using safe DOM APIs (no innerHTML for user content)
1811
+ this.buildFormDOM(form, container);
1583
1812
  overlay.appendChild(container);
1584
1813
  document.body.appendChild(overlay);
1585
1814
  // Animate in
@@ -1591,95 +1820,162 @@
1591
1820
  // Setup event listeners
1592
1821
  this.setupFormEvents(form, overlay, container);
1593
1822
  }
1594
- buildFormHTML(form) {
1823
+ /**
1824
+ * Escape HTML to prevent XSS - used only for static structure
1825
+ */
1826
+ escapeHTML(str) {
1827
+ const div = document.createElement('div');
1828
+ div.textContent = str;
1829
+ return div.innerHTML;
1830
+ }
1831
+ /**
1832
+ * Build form using safe DOM APIs (prevents XSS)
1833
+ */
1834
+ buildFormDOM(form, container) {
1595
1835
  const style = form.style || {};
1596
1836
  const primaryColor = style.primaryColor || '#10B981';
1597
1837
  const textColor = style.textColor || '#18181B';
1598
- let fieldsHTML = form.fields.map(field => {
1599
- const requiredMark = field.required ? '<span style="color: #EF4444;">*</span>' : '';
1600
- if (field.type === 'textarea') {
1601
- return `
1602
- <div style="margin-bottom: 12px;">
1603
- <label style="display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; color: ${textColor};">
1604
- ${field.label} ${requiredMark}
1605
- </label>
1606
- <textarea
1607
- name="${field.name}"
1608
- placeholder="${field.placeholder || ''}"
1609
- ${field.required ? 'required' : ''}
1610
- style="width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; resize: vertical; min-height: 80px;"
1611
- ></textarea>
1612
- </div>
1613
- `;
1614
- }
1615
- else if (field.type === 'checkbox') {
1616
- return `
1617
- <div style="margin-bottom: 12px;">
1618
- <label style="display: flex; align-items: center; gap: 8px; font-size: 14px; color: ${textColor}; cursor: pointer;">
1619
- <input
1620
- type="checkbox"
1621
- name="${field.name}"
1622
- ${field.required ? 'required' : ''}
1623
- style="width: 16px; height: 16px;"
1624
- />
1625
- ${field.label} ${requiredMark}
1626
- </label>
1627
- </div>
1628
- `;
1838
+ // Close button
1839
+ const closeBtn = document.createElement('button');
1840
+ closeBtn.id = 'clianta-form-close';
1841
+ closeBtn.style.cssText = `
1842
+ position: absolute;
1843
+ top: 12px;
1844
+ right: 12px;
1845
+ background: none;
1846
+ border: none;
1847
+ font-size: 20px;
1848
+ cursor: pointer;
1849
+ color: #71717A;
1850
+ padding: 4px;
1851
+ `;
1852
+ closeBtn.textContent = '×';
1853
+ container.appendChild(closeBtn);
1854
+ // Headline
1855
+ const headline = document.createElement('h2');
1856
+ headline.style.cssText = `font-size: 20px; font-weight: 700; margin-bottom: 8px; color: ${this.escapeHTML(textColor)};`;
1857
+ headline.textContent = form.headline || 'Stay in touch';
1858
+ container.appendChild(headline);
1859
+ // Subheadline
1860
+ const subheadline = document.createElement('p');
1861
+ subheadline.style.cssText = 'font-size: 14px; color: #71717A; margin-bottom: 16px;';
1862
+ subheadline.textContent = form.subheadline || 'Get the latest updates';
1863
+ container.appendChild(subheadline);
1864
+ // Form element
1865
+ const formElement = document.createElement('form');
1866
+ formElement.id = 'clianta-form-element';
1867
+ // Build fields
1868
+ form.fields.forEach(field => {
1869
+ const fieldWrapper = document.createElement('div');
1870
+ fieldWrapper.style.marginBottom = '12px';
1871
+ if (field.type === 'checkbox') {
1872
+ // Checkbox layout
1873
+ const label = document.createElement('label');
1874
+ label.style.cssText = `display: flex; align-items: center; gap: 8px; font-size: 14px; color: ${this.escapeHTML(textColor)}; cursor: pointer;`;
1875
+ const input = document.createElement('input');
1876
+ input.type = 'checkbox';
1877
+ input.name = field.name;
1878
+ if (field.required)
1879
+ input.required = true;
1880
+ input.style.cssText = 'width: 16px; height: 16px;';
1881
+ label.appendChild(input);
1882
+ const labelText = document.createTextNode(field.label + ' ');
1883
+ label.appendChild(labelText);
1884
+ if (field.required) {
1885
+ const requiredMark = document.createElement('span');
1886
+ requiredMark.style.color = '#EF4444';
1887
+ requiredMark.textContent = '*';
1888
+ label.appendChild(requiredMark);
1889
+ }
1890
+ fieldWrapper.appendChild(label);
1629
1891
  }
1630
1892
  else {
1631
- return `
1632
- <div style="margin-bottom: 12px;">
1633
- <label style="display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; color: ${textColor};">
1634
- ${field.label} ${requiredMark}
1635
- </label>
1636
- <input
1637
- type="${field.type}"
1638
- name="${field.name}"
1639
- placeholder="${field.placeholder || ''}"
1640
- ${field.required ? 'required' : ''}
1641
- style="width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; box-sizing: border-box;"
1642
- />
1643
- </div>
1644
- `;
1893
+ // Label
1894
+ const label = document.createElement('label');
1895
+ label.style.cssText = `display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; color: ${this.escapeHTML(textColor)};`;
1896
+ label.textContent = field.label + ' ';
1897
+ if (field.required) {
1898
+ const requiredMark = document.createElement('span');
1899
+ requiredMark.style.color = '#EF4444';
1900
+ requiredMark.textContent = '*';
1901
+ label.appendChild(requiredMark);
1902
+ }
1903
+ fieldWrapper.appendChild(label);
1904
+ // Input/Textarea/Select
1905
+ if (field.type === 'textarea') {
1906
+ const textarea = document.createElement('textarea');
1907
+ textarea.name = field.name;
1908
+ if (field.placeholder)
1909
+ textarea.placeholder = field.placeholder;
1910
+ if (field.required)
1911
+ textarea.required = true;
1912
+ textarea.style.cssText = 'width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; resize: vertical; min-height: 80px; box-sizing: border-box;';
1913
+ fieldWrapper.appendChild(textarea);
1914
+ }
1915
+ else if (field.type === 'select') {
1916
+ const select = document.createElement('select');
1917
+ select.name = field.name;
1918
+ if (field.required)
1919
+ select.required = true;
1920
+ select.style.cssText = 'width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; box-sizing: border-box; background: white; cursor: pointer;';
1921
+ // Add placeholder option
1922
+ if (field.placeholder) {
1923
+ const placeholderOption = document.createElement('option');
1924
+ placeholderOption.value = '';
1925
+ placeholderOption.textContent = field.placeholder;
1926
+ placeholderOption.disabled = true;
1927
+ placeholderOption.selected = true;
1928
+ select.appendChild(placeholderOption);
1929
+ }
1930
+ // Add options from field.options array if provided
1931
+ if (field.options && Array.isArray(field.options)) {
1932
+ field.options.forEach((opt) => {
1933
+ const option = document.createElement('option');
1934
+ if (typeof opt === 'string') {
1935
+ option.value = opt;
1936
+ option.textContent = opt;
1937
+ }
1938
+ else {
1939
+ option.value = opt.value;
1940
+ option.textContent = opt.label;
1941
+ }
1942
+ select.appendChild(option);
1943
+ });
1944
+ }
1945
+ fieldWrapper.appendChild(select);
1946
+ }
1947
+ else {
1948
+ const input = document.createElement('input');
1949
+ input.type = field.type;
1950
+ input.name = field.name;
1951
+ if (field.placeholder)
1952
+ input.placeholder = field.placeholder;
1953
+ if (field.required)
1954
+ input.required = true;
1955
+ input.style.cssText = 'width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; box-sizing: border-box;';
1956
+ fieldWrapper.appendChild(input);
1957
+ }
1645
1958
  }
1646
- }).join('');
1647
- return `
1648
- <button id="clianta-form-close" style="
1649
- position: absolute;
1650
- top: 12px;
1651
- right: 12px;
1652
- background: none;
1653
- border: none;
1654
- font-size: 20px;
1655
- cursor: pointer;
1656
- color: #71717A;
1657
- padding: 4px;
1658
- ">&times;</button>
1659
- <h2 style="font-size: 20px; font-weight: 700; margin-bottom: 8px; color: ${textColor};">
1660
- ${form.headline || 'Stay in touch'}
1661
- </h2>
1662
- <p style="font-size: 14px; color: #71717A; margin-bottom: 16px;">
1663
- ${form.subheadline || 'Get the latest updates'}
1664
- </p>
1665
- <form id="clianta-form-element">
1666
- ${fieldsHTML}
1667
- <button type="submit" style="
1668
- width: 100%;
1669
- padding: 10px 16px;
1670
- background: ${primaryColor};
1671
- color: white;
1672
- border: none;
1673
- border-radius: 6px;
1674
- font-size: 14px;
1675
- font-weight: 500;
1676
- cursor: pointer;
1677
- margin-top: 8px;
1678
- ">
1679
- ${form.submitButtonText || 'Subscribe'}
1680
- </button>
1681
- </form>
1959
+ formElement.appendChild(fieldWrapper);
1960
+ });
1961
+ // Submit button
1962
+ const submitBtn = document.createElement('button');
1963
+ submitBtn.type = 'submit';
1964
+ submitBtn.style.cssText = `
1965
+ width: 100%;
1966
+ padding: 10px 16px;
1967
+ background: ${this.escapeHTML(primaryColor)};
1968
+ color: white;
1969
+ border: none;
1970
+ border-radius: 6px;
1971
+ font-size: 14px;
1972
+ font-weight: 500;
1973
+ cursor: pointer;
1974
+ margin-top: 8px;
1682
1975
  `;
1976
+ submitBtn.textContent = form.submitButtonText || 'Subscribe';
1977
+ formElement.appendChild(submitBtn);
1978
+ container.appendChild(formElement);
1683
1979
  }
1684
1980
  setupFormEvents(form, overlay, container) {
1685
1981
  // Close button
@@ -1740,19 +2036,29 @@
1740
2036
  });
1741
2037
  const result = await response.json();
1742
2038
  if (result.success) {
1743
- // Show success message
1744
- container.innerHTML = `
1745
- <div style="text-align: center; padding: 20px;">
1746
- <div style="width: 48px; height: 48px; background: #10B981; border-radius: 50%; margin: 0 auto 16px; display: flex; align-items: center; justify-content: center;">
1747
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
1748
- <polyline points="20 6 9 17 4 12"></polyline>
1749
- </svg>
1750
- </div>
1751
- <p style="font-size: 16px; font-weight: 500; color: #18181B;">
1752
- ${form.successMessage || 'Thank you!'}
1753
- </p>
1754
- </div>
1755
- `;
2039
+ // Show success message using safe DOM APIs
2040
+ container.innerHTML = '';
2041
+ const successWrapper = document.createElement('div');
2042
+ successWrapper.style.cssText = 'text-align: center; padding: 20px;';
2043
+ const iconWrapper = document.createElement('div');
2044
+ iconWrapper.style.cssText = 'width: 48px; height: 48px; background: #10B981; border-radius: 50%; margin: 0 auto 16px; display: flex; align-items: center; justify-content: center;';
2045
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2046
+ svg.setAttribute('width', '24');
2047
+ svg.setAttribute('height', '24');
2048
+ svg.setAttribute('viewBox', '0 0 24 24');
2049
+ svg.setAttribute('fill', 'none');
2050
+ svg.setAttribute('stroke', 'white');
2051
+ svg.setAttribute('stroke-width', '2');
2052
+ const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
2053
+ polyline.setAttribute('points', '20 6 9 17 4 12');
2054
+ svg.appendChild(polyline);
2055
+ iconWrapper.appendChild(svg);
2056
+ const message = document.createElement('p');
2057
+ message.style.cssText = 'font-size: 16px; font-weight: 500; color: #18181B;';
2058
+ message.textContent = form.successMessage || 'Thank you!';
2059
+ successWrapper.appendChild(iconWrapper);
2060
+ successWrapper.appendChild(message);
2061
+ container.appendChild(successWrapper);
1756
2062
  // Track identify
1757
2063
  if (data.email) {
1758
2064
  this.tracker?.identify(data.email, data);
@@ -1889,6 +2195,8 @@
1889
2195
  * Manages consent state and event buffering for GDPR/CCPA compliance
1890
2196
  * @see SDK_VERSION in core/config.ts
1891
2197
  */
2198
+ /** Maximum events to buffer while waiting for consent */
2199
+ const MAX_BUFFER_SIZE = 100;
1892
2200
  /**
1893
2201
  * Manages user consent state for tracking
1894
2202
  */
@@ -2005,6 +2313,11 @@
2005
2313
  * Buffer an event (for waitForConsent mode)
2006
2314
  */
2007
2315
  bufferEvent(event) {
2316
+ // Prevent unbounded buffer growth
2317
+ if (this.eventBuffer.length >= MAX_BUFFER_SIZE) {
2318
+ logger.warn('Consent event buffer full, dropping oldest event');
2319
+ this.eventBuffer.shift();
2320
+ }
2008
2321
  this.eventBuffer.push(event);
2009
2322
  logger.debug('Event buffered (waiting for consent):', event.eventName);
2010
2323
  }
@@ -2064,6 +2377,8 @@
2064
2377
  constructor(workspaceId, userConfig = {}) {
2065
2378
  this.plugins = [];
2066
2379
  this.isInitialized = false;
2380
+ /** Pending identify retry on next flush */
2381
+ this.pendingIdentify = null;
2067
2382
  if (!workspaceId) {
2068
2383
  throw new Error('[Clianta] Workspace ID is required');
2069
2384
  }
@@ -2149,6 +2464,7 @@
2149
2464
  }
2150
2465
  /**
2151
2466
  * Initialize enabled plugins
2467
+ * Handles both sync and async plugin init methods
2152
2468
  */
2153
2469
  initPlugins() {
2154
2470
  const pluginsToLoad = this.config.plugins;
@@ -2159,7 +2475,13 @@
2159
2475
  for (const pluginName of filteredPlugins) {
2160
2476
  try {
2161
2477
  const plugin = getPlugin(pluginName);
2162
- plugin.init(this);
2478
+ // Handle both sync and async init (fire-and-forget for async)
2479
+ const result = plugin.init(this);
2480
+ if (result instanceof Promise) {
2481
+ result.catch((error) => {
2482
+ logger.error(`Async plugin init failed: ${pluginName}`, error);
2483
+ });
2484
+ }
2163
2485
  this.plugins.push(plugin);
2164
2486
  logger.debug(`Plugin loaded: ${pluginName}`);
2165
2487
  }
@@ -2186,7 +2508,7 @@
2186
2508
  referrer: typeof document !== 'undefined' ? document.referrer || undefined : undefined,
2187
2509
  properties,
2188
2510
  device: getDeviceInfo(),
2189
- utm: getUTMParams(),
2511
+ ...getUTMParams(),
2190
2512
  timestamp: new Date().toISOString(),
2191
2513
  sdkVersion: SDK_VERSION,
2192
2514
  };
@@ -2231,11 +2553,24 @@
2231
2553
  });
2232
2554
  if (result.success) {
2233
2555
  logger.info('Visitor identified successfully');
2556
+ this.pendingIdentify = null;
2234
2557
  }
2235
2558
  else {
2236
2559
  logger.error('Failed to identify visitor:', result.error);
2560
+ // Store for retry on next flush
2561
+ this.pendingIdentify = { email, traits };
2237
2562
  }
2238
2563
  }
2564
+ /**
2565
+ * Retry pending identify call
2566
+ */
2567
+ async retryPendingIdentify() {
2568
+ if (!this.pendingIdentify)
2569
+ return;
2570
+ const { email, traits } = this.pendingIdentify;
2571
+ this.pendingIdentify = null;
2572
+ await this.identify(email, traits);
2573
+ }
2239
2574
  /**
2240
2575
  * Update consent state
2241
2576
  */
@@ -2283,6 +2618,7 @@
2283
2618
  * Force flush event queue
2284
2619
  */
2285
2620
  async flush() {
2621
+ await this.retryPendingIdentify();
2286
2622
  await this.queue.flush();
2287
2623
  }
2288
2624
  /**
@@ -2337,10 +2673,10 @@
2337
2673
  /**
2338
2674
  * Destroy tracker and cleanup
2339
2675
  */
2340
- destroy() {
2676
+ async destroy() {
2341
2677
  logger.info('Destroying tracker');
2342
- // Flush any remaining events
2343
- this.queue.flush();
2678
+ // Flush any remaining events (await to ensure completion)
2679
+ await this.queue.flush();
2344
2680
  // Destroy plugins
2345
2681
  for (const plugin of this.plugins) {
2346
2682
  if (plugin.destroy) {
@@ -2355,20 +2691,28 @@
2355
2691
  }
2356
2692
 
2357
2693
  /**
2358
- * Clianta SDK - CRM API Client
2359
- * @see SDK_VERSION in core/config.ts
2694
+ * Clianta SDK - Event Triggers Manager
2695
+ * Manages event-driven automation and email notifications
2360
2696
  */
2361
2697
  /**
2362
- * CRM API Client for managing contacts and opportunities
2698
+ * Event Triggers Manager
2699
+ * Handles event-driven automation based on CRM actions
2700
+ *
2701
+ * Similar to:
2702
+ * - Salesforce: Process Builder, Flow Automation
2703
+ * - HubSpot: Workflows, Email Sequences
2704
+ * - Pipedrive: Workflow Automation
2363
2705
  */
2364
- class CRMClient {
2706
+ class EventTriggersManager {
2365
2707
  constructor(apiEndpoint, workspaceId, authToken) {
2708
+ this.triggers = new Map();
2709
+ this.listeners = new Map();
2366
2710
  this.apiEndpoint = apiEndpoint;
2367
2711
  this.workspaceId = workspaceId;
2368
2712
  this.authToken = authToken;
2369
2713
  }
2370
2714
  /**
2371
- * Set authentication token for API requests
2715
+ * Set authentication token
2372
2716
  */
2373
2717
  setAuthToken(token) {
2374
2718
  this.authToken = token;
@@ -2413,120 +2757,942 @@
2413
2757
  }
2414
2758
  }
2415
2759
  // ============================================
2416
- // CONTACTS API
2760
+ // TRIGGER MANAGEMENT
2417
2761
  // ============================================
2418
2762
  /**
2419
- * Get all contacts with pagination
2763
+ * Get all event triggers
2420
2764
  */
2421
- async getContacts(params) {
2422
- const queryParams = new URLSearchParams();
2423
- if (params?.page)
2424
- queryParams.set('page', params.page.toString());
2425
- if (params?.limit)
2426
- queryParams.set('limit', params.limit.toString());
2427
- if (params?.search)
2428
- queryParams.set('search', params.search);
2429
- if (params?.status)
2430
- queryParams.set('status', params.status);
2431
- const query = queryParams.toString();
2432
- const endpoint = `/api/workspaces/${this.workspaceId}/contacts${query ? `?${query}` : ''}`;
2433
- return this.request(endpoint);
2765
+ async getTriggers() {
2766
+ return this.request(`/api/workspaces/${this.workspaceId}/triggers`);
2434
2767
  }
2435
2768
  /**
2436
- * Get a single contact by ID
2769
+ * Get a single trigger by ID
2437
2770
  */
2438
- async getContact(contactId) {
2439
- return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`);
2771
+ async getTrigger(triggerId) {
2772
+ return this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`);
2440
2773
  }
2441
2774
  /**
2442
- * Create a new contact
2775
+ * Create a new event trigger
2443
2776
  */
2444
- async createContact(contact) {
2445
- return this.request(`/api/workspaces/${this.workspaceId}/contacts`, {
2777
+ async createTrigger(trigger) {
2778
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers`, {
2446
2779
  method: 'POST',
2447
- body: JSON.stringify(contact),
2780
+ body: JSON.stringify(trigger),
2448
2781
  });
2782
+ // Cache the trigger locally if successful
2783
+ if (result.success && result.data?._id) {
2784
+ this.triggers.set(result.data._id, result.data);
2785
+ }
2786
+ return result;
2449
2787
  }
2450
2788
  /**
2451
- * Update an existing contact
2789
+ * Update an existing trigger
2452
2790
  */
2453
- async updateContact(contactId, updates) {
2454
- return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2791
+ async updateTrigger(triggerId, updates) {
2792
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
2455
2793
  method: 'PUT',
2456
2794
  body: JSON.stringify(updates),
2457
2795
  });
2796
+ // Update cache if successful
2797
+ if (result.success && result.data?._id) {
2798
+ this.triggers.set(result.data._id, result.data);
2799
+ }
2800
+ return result;
2458
2801
  }
2459
2802
  /**
2460
- * Delete a contact
2803
+ * Delete a trigger
2461
2804
  */
2462
- async deleteContact(contactId) {
2463
- return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2805
+ async deleteTrigger(triggerId) {
2806
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
2464
2807
  method: 'DELETE',
2465
2808
  });
2809
+ // Remove from cache if successful
2810
+ if (result.success) {
2811
+ this.triggers.delete(triggerId);
2812
+ }
2813
+ return result;
2814
+ }
2815
+ /**
2816
+ * Activate a trigger
2817
+ */
2818
+ async activateTrigger(triggerId) {
2819
+ return this.updateTrigger(triggerId, { isActive: true });
2820
+ }
2821
+ /**
2822
+ * Deactivate a trigger
2823
+ */
2824
+ async deactivateTrigger(triggerId) {
2825
+ return this.updateTrigger(triggerId, { isActive: false });
2466
2826
  }
2467
2827
  // ============================================
2468
- // OPPORTUNITIES API
2828
+ // EVENT HANDLING (CLIENT-SIDE)
2469
2829
  // ============================================
2470
2830
  /**
2471
- * Get all opportunities with pagination
2831
+ * Register a local event listener for client-side triggers
2832
+ * This allows immediate client-side reactions to events
2472
2833
  */
2473
- async getOpportunities(params) {
2474
- const queryParams = new URLSearchParams();
2475
- if (params?.page)
2476
- queryParams.set('page', params.page.toString());
2477
- if (params?.limit)
2478
- queryParams.set('limit', params.limit.toString());
2479
- if (params?.pipelineId)
2480
- queryParams.set('pipelineId', params.pipelineId);
2481
- if (params?.stageId)
2482
- queryParams.set('stageId', params.stageId);
2483
- const query = queryParams.toString();
2484
- const endpoint = `/api/workspaces/${this.workspaceId}/opportunities${query ? `?${query}` : ''}`;
2485
- return this.request(endpoint);
2834
+ on(eventType, callback) {
2835
+ if (!this.listeners.has(eventType)) {
2836
+ this.listeners.set(eventType, new Set());
2837
+ }
2838
+ this.listeners.get(eventType).add(callback);
2839
+ logger.debug(`Event listener registered: ${eventType}`);
2486
2840
  }
2487
2841
  /**
2488
- * Get a single opportunity by ID
2842
+ * Remove an event listener
2489
2843
  */
2490
- async getOpportunity(opportunityId) {
2491
- return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`);
2844
+ off(eventType, callback) {
2845
+ const listeners = this.listeners.get(eventType);
2846
+ if (listeners) {
2847
+ listeners.delete(callback);
2848
+ }
2492
2849
  }
2493
2850
  /**
2494
- * Create a new opportunity
2851
+ * Emit an event (client-side only)
2852
+ * This will trigger any registered local listeners
2495
2853
  */
2496
- async createOpportunity(opportunity) {
2497
- return this.request(`/api/workspaces/${this.workspaceId}/opportunities`, {
2854
+ emit(eventType, data) {
2855
+ logger.debug(`Event emitted: ${eventType}`, data);
2856
+ const listeners = this.listeners.get(eventType);
2857
+ if (listeners) {
2858
+ listeners.forEach(callback => {
2859
+ try {
2860
+ callback(data);
2861
+ }
2862
+ catch (error) {
2863
+ logger.error(`Error in event listener for ${eventType}:`, error);
2864
+ }
2865
+ });
2866
+ }
2867
+ }
2868
+ /**
2869
+ * Check if conditions are met for a trigger
2870
+ * Supports dynamic field evaluation including custom fields and nested paths
2871
+ */
2872
+ evaluateConditions(conditions, data) {
2873
+ if (!conditions || conditions.length === 0) {
2874
+ return true; // No conditions means always fire
2875
+ }
2876
+ return conditions.every(condition => {
2877
+ // Support dot notation for nested fields (e.g., 'customFields.industry')
2878
+ const fieldValue = condition.field.includes('.')
2879
+ ? this.getNestedValue(data, condition.field)
2880
+ : data[condition.field];
2881
+ const targetValue = condition.value;
2882
+ switch (condition.operator) {
2883
+ case 'equals':
2884
+ return fieldValue === targetValue;
2885
+ case 'not_equals':
2886
+ return fieldValue !== targetValue;
2887
+ case 'contains':
2888
+ return String(fieldValue).includes(String(targetValue));
2889
+ case 'greater_than':
2890
+ return Number(fieldValue) > Number(targetValue);
2891
+ case 'less_than':
2892
+ return Number(fieldValue) < Number(targetValue);
2893
+ case 'in':
2894
+ return Array.isArray(targetValue) && targetValue.includes(fieldValue);
2895
+ case 'not_in':
2896
+ return Array.isArray(targetValue) && !targetValue.includes(fieldValue);
2897
+ default:
2898
+ return false;
2899
+ }
2900
+ });
2901
+ }
2902
+ /**
2903
+ * Execute actions for a triggered event (client-side preview)
2904
+ * Note: Actual execution happens on the backend
2905
+ */
2906
+ async executeActions(trigger, data) {
2907
+ logger.info(`Executing actions for trigger: ${trigger.name}`);
2908
+ for (const action of trigger.actions) {
2909
+ try {
2910
+ await this.executeAction(action, data);
2911
+ }
2912
+ catch (error) {
2913
+ logger.error(`Failed to execute action:`, error);
2914
+ }
2915
+ }
2916
+ }
2917
+ /**
2918
+ * Execute a single action
2919
+ */
2920
+ async executeAction(action, data) {
2921
+ switch (action.type) {
2922
+ case 'send_email':
2923
+ await this.executeSendEmail(action, data);
2924
+ break;
2925
+ case 'webhook':
2926
+ await this.executeWebhook(action, data);
2927
+ break;
2928
+ case 'create_task':
2929
+ await this.executeCreateTask(action, data);
2930
+ break;
2931
+ case 'update_contact':
2932
+ await this.executeUpdateContact(action, data);
2933
+ break;
2934
+ default:
2935
+ logger.warn(`Unknown action type:`, action);
2936
+ }
2937
+ }
2938
+ /**
2939
+ * Execute send email action (via backend API)
2940
+ */
2941
+ async executeSendEmail(action, data) {
2942
+ logger.debug('Sending email:', action);
2943
+ const payload = {
2944
+ to: this.replaceVariables(action.to, data),
2945
+ subject: action.subject ? this.replaceVariables(action.subject, data) : undefined,
2946
+ body: action.body ? this.replaceVariables(action.body, data) : undefined,
2947
+ templateId: action.templateId,
2948
+ cc: action.cc,
2949
+ bcc: action.bcc,
2950
+ from: action.from,
2951
+ delayMinutes: action.delayMinutes,
2952
+ };
2953
+ await this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
2498
2954
  method: 'POST',
2499
- body: JSON.stringify(opportunity),
2955
+ body: JSON.stringify(payload),
2500
2956
  });
2501
2957
  }
2502
2958
  /**
2503
- * Update an existing opportunity
2959
+ * Execute webhook action
2504
2960
  */
2505
- async updateOpportunity(opportunityId, updates) {
2506
- return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
2961
+ async executeWebhook(action, data) {
2962
+ logger.debug('Calling webhook:', action.url);
2963
+ const body = action.body ? this.replaceVariables(action.body, data) : JSON.stringify(data);
2964
+ await fetch(action.url, {
2965
+ method: action.method,
2966
+ headers: {
2967
+ 'Content-Type': 'application/json',
2968
+ ...action.headers,
2969
+ },
2970
+ body,
2971
+ });
2972
+ }
2973
+ /**
2974
+ * Execute create task action
2975
+ */
2976
+ async executeCreateTask(action, data) {
2977
+ logger.debug('Creating task:', action.title);
2978
+ const dueDate = action.dueDays
2979
+ ? new Date(Date.now() + action.dueDays * 24 * 60 * 60 * 1000).toISOString()
2980
+ : undefined;
2981
+ await this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
2982
+ method: 'POST',
2983
+ body: JSON.stringify({
2984
+ title: this.replaceVariables(action.title, data),
2985
+ description: action.description ? this.replaceVariables(action.description, data) : undefined,
2986
+ priority: action.priority,
2987
+ dueDate,
2988
+ assignedTo: action.assignedTo,
2989
+ relatedContactId: typeof data.contactId === 'string' ? data.contactId : undefined,
2990
+ }),
2991
+ });
2992
+ }
2993
+ /**
2994
+ * Execute update contact action
2995
+ */
2996
+ async executeUpdateContact(action, data) {
2997
+ const contactId = data.contactId || data._id;
2998
+ if (!contactId) {
2999
+ logger.warn('Cannot update contact: no contactId in data');
3000
+ return;
3001
+ }
3002
+ logger.debug('Updating contact:', contactId);
3003
+ await this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2507
3004
  method: 'PUT',
2508
- body: JSON.stringify(updates),
3005
+ body: JSON.stringify(action.updates),
2509
3006
  });
2510
3007
  }
2511
3008
  /**
2512
- * Delete an opportunity
3009
+ * Replace variables in a string template
3010
+ * Supports syntax like {{contact.email}}, {{opportunity.value}}
2513
3011
  */
2514
- async deleteOpportunity(opportunityId) {
2515
- return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
2516
- method: 'DELETE',
3012
+ replaceVariables(template, data) {
3013
+ return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
3014
+ const value = this.getNestedValue(data, path.trim());
3015
+ return value !== undefined ? String(value) : match;
2517
3016
  });
2518
3017
  }
2519
3018
  /**
2520
- * Move opportunity to a different stage
3019
+ * Get nested value from object using dot notation
3020
+ * Supports dynamic field access including custom fields
2521
3021
  */
2522
- async moveOpportunity(opportunityId, stageId) {
2523
- return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/move`, {
2524
- method: 'POST',
2525
- body: JSON.stringify({ stageId }),
3022
+ getNestedValue(obj, path) {
3023
+ return path.split('.').reduce((current, key) => {
3024
+ return current !== null && current !== undefined && typeof current === 'object'
3025
+ ? current[key]
3026
+ : undefined;
3027
+ }, obj);
3028
+ }
3029
+ /**
3030
+ * Extract all available field paths from a data object
3031
+ * Useful for dynamic field discovery based on platform-specific attributes
3032
+ * @param obj - The data object to extract fields from
3033
+ * @param prefix - Internal use for nested paths
3034
+ * @param maxDepth - Maximum depth to traverse (default: 3)
3035
+ * @returns Array of field paths (e.g., ['email', 'contact.firstName', 'customFields.industry'])
3036
+ */
3037
+ extractAvailableFields(obj, prefix = '', maxDepth = 3) {
3038
+ if (maxDepth <= 0)
3039
+ return [];
3040
+ const fields = [];
3041
+ for (const key in obj) {
3042
+ if (!obj.hasOwnProperty(key))
3043
+ continue;
3044
+ const value = obj[key];
3045
+ const fieldPath = prefix ? `${prefix}.${key}` : key;
3046
+ fields.push(fieldPath);
3047
+ // Recursively traverse nested objects
3048
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
3049
+ const nestedFields = this.extractAvailableFields(value, fieldPath, maxDepth - 1);
3050
+ fields.push(...nestedFields);
3051
+ }
3052
+ }
3053
+ return fields;
3054
+ }
3055
+ /**
3056
+ * Get available fields from sample data
3057
+ * Helps with dynamic field detection for platform-specific attributes
3058
+ * @param sampleData - Sample data object to analyze
3059
+ * @returns Array of available field paths
3060
+ */
3061
+ getAvailableFields(sampleData) {
3062
+ return this.extractAvailableFields(sampleData);
3063
+ }
3064
+ // ============================================
3065
+ // HELPER METHODS FOR COMMON PATTERNS
3066
+ // ============================================
3067
+ /**
3068
+ * Create a simple email trigger
3069
+ * Helper method for common use case
3070
+ */
3071
+ async createEmailTrigger(config) {
3072
+ return this.createTrigger({
3073
+ name: config.name,
3074
+ eventType: config.eventType,
3075
+ conditions: config.conditions,
3076
+ actions: [
3077
+ {
3078
+ type: 'send_email',
3079
+ to: config.to,
3080
+ subject: config.subject,
3081
+ body: config.body,
3082
+ },
3083
+ ],
3084
+ isActive: true,
3085
+ });
3086
+ }
3087
+ /**
3088
+ * Create a task creation trigger
3089
+ */
3090
+ async createTaskTrigger(config) {
3091
+ return this.createTrigger({
3092
+ name: config.name,
3093
+ eventType: config.eventType,
3094
+ conditions: config.conditions,
3095
+ actions: [
3096
+ {
3097
+ type: 'create_task',
3098
+ title: config.taskTitle,
3099
+ description: config.taskDescription,
3100
+ priority: config.priority,
3101
+ dueDays: config.dueDays,
3102
+ },
3103
+ ],
3104
+ isActive: true,
3105
+ });
3106
+ }
3107
+ /**
3108
+ * Create a webhook trigger
3109
+ */
3110
+ async createWebhookTrigger(config) {
3111
+ return this.createTrigger({
3112
+ name: config.name,
3113
+ eventType: config.eventType,
3114
+ conditions: config.conditions,
3115
+ actions: [
3116
+ {
3117
+ type: 'webhook',
3118
+ url: config.webhookUrl,
3119
+ method: config.method || 'POST',
3120
+ },
3121
+ ],
3122
+ isActive: true,
2526
3123
  });
2527
3124
  }
2528
3125
  }
2529
3126
 
3127
+ /**
3128
+ * Clianta SDK - CRM API Client
3129
+ * @see SDK_VERSION in core/config.ts
3130
+ */
3131
+ /**
3132
+ * CRM API Client for managing contacts and opportunities
3133
+ */
3134
+ class CRMClient {
3135
+ constructor(apiEndpoint, workspaceId, authToken) {
3136
+ this.apiEndpoint = apiEndpoint;
3137
+ this.workspaceId = workspaceId;
3138
+ this.authToken = authToken;
3139
+ this.triggers = new EventTriggersManager(apiEndpoint, workspaceId, authToken);
3140
+ }
3141
+ /**
3142
+ * Set authentication token for API requests
3143
+ */
3144
+ setAuthToken(token) {
3145
+ this.authToken = token;
3146
+ this.triggers.setAuthToken(token);
3147
+ }
3148
+ /**
3149
+ * Validate required parameter exists
3150
+ * @throws {Error} if value is null/undefined or empty string
3151
+ */
3152
+ validateRequired(param, value, methodName) {
3153
+ if (value === null || value === undefined || value === '') {
3154
+ throw new Error(`[CRMClient.${methodName}] ${param} is required`);
3155
+ }
3156
+ }
3157
+ /**
3158
+ * Make authenticated API request
3159
+ */
3160
+ async request(endpoint, options = {}) {
3161
+ const url = `${this.apiEndpoint}${endpoint}`;
3162
+ const headers = {
3163
+ 'Content-Type': 'application/json',
3164
+ ...(options.headers || {}),
3165
+ };
3166
+ if (this.authToken) {
3167
+ headers['Authorization'] = `Bearer ${this.authToken}`;
3168
+ }
3169
+ try {
3170
+ const response = await fetch(url, {
3171
+ ...options,
3172
+ headers,
3173
+ });
3174
+ const data = await response.json();
3175
+ if (!response.ok) {
3176
+ return {
3177
+ success: false,
3178
+ error: data.message || 'Request failed',
3179
+ status: response.status,
3180
+ };
3181
+ }
3182
+ return {
3183
+ success: true,
3184
+ data: data.data || data,
3185
+ status: response.status,
3186
+ };
3187
+ }
3188
+ catch (error) {
3189
+ return {
3190
+ success: false,
3191
+ error: error instanceof Error ? error.message : 'Network error',
3192
+ status: 0,
3193
+ };
3194
+ }
3195
+ }
3196
+ // ============================================
3197
+ // CONTACTS API
3198
+ // ============================================
3199
+ /**
3200
+ * Get all contacts with pagination
3201
+ */
3202
+ async getContacts(params) {
3203
+ const queryParams = new URLSearchParams();
3204
+ if (params?.page)
3205
+ queryParams.set('page', params.page.toString());
3206
+ if (params?.limit)
3207
+ queryParams.set('limit', params.limit.toString());
3208
+ if (params?.search)
3209
+ queryParams.set('search', params.search);
3210
+ if (params?.status)
3211
+ queryParams.set('status', params.status);
3212
+ const query = queryParams.toString();
3213
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts${query ? `?${query}` : ''}`;
3214
+ return this.request(endpoint);
3215
+ }
3216
+ /**
3217
+ * Get a single contact by ID
3218
+ */
3219
+ async getContact(contactId) {
3220
+ this.validateRequired('contactId', contactId, 'getContact');
3221
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`);
3222
+ }
3223
+ /**
3224
+ * Create a new contact
3225
+ */
3226
+ async createContact(contact) {
3227
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts`, {
3228
+ method: 'POST',
3229
+ body: JSON.stringify(contact),
3230
+ });
3231
+ }
3232
+ /**
3233
+ * Update an existing contact
3234
+ */
3235
+ async updateContact(contactId, updates) {
3236
+ this.validateRequired('contactId', contactId, 'updateContact');
3237
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
3238
+ method: 'PUT',
3239
+ body: JSON.stringify(updates),
3240
+ });
3241
+ }
3242
+ /**
3243
+ * Delete a contact
3244
+ */
3245
+ async deleteContact(contactId) {
3246
+ this.validateRequired('contactId', contactId, 'deleteContact');
3247
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
3248
+ method: 'DELETE',
3249
+ });
3250
+ }
3251
+ // ============================================
3252
+ // OPPORTUNITIES API
3253
+ // ============================================
3254
+ /**
3255
+ * Get all opportunities with pagination
3256
+ */
3257
+ async getOpportunities(params) {
3258
+ const queryParams = new URLSearchParams();
3259
+ if (params?.page)
3260
+ queryParams.set('page', params.page.toString());
3261
+ if (params?.limit)
3262
+ queryParams.set('limit', params.limit.toString());
3263
+ if (params?.pipelineId)
3264
+ queryParams.set('pipelineId', params.pipelineId);
3265
+ if (params?.stageId)
3266
+ queryParams.set('stageId', params.stageId);
3267
+ const query = queryParams.toString();
3268
+ const endpoint = `/api/workspaces/${this.workspaceId}/opportunities${query ? `?${query}` : ''}`;
3269
+ return this.request(endpoint);
3270
+ }
3271
+ /**
3272
+ * Get a single opportunity by ID
3273
+ */
3274
+ async getOpportunity(opportunityId) {
3275
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`);
3276
+ }
3277
+ /**
3278
+ * Create a new opportunity
3279
+ */
3280
+ async createOpportunity(opportunity) {
3281
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities`, {
3282
+ method: 'POST',
3283
+ body: JSON.stringify(opportunity),
3284
+ });
3285
+ }
3286
+ /**
3287
+ * Update an existing opportunity
3288
+ */
3289
+ async updateOpportunity(opportunityId, updates) {
3290
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
3291
+ method: 'PUT',
3292
+ body: JSON.stringify(updates),
3293
+ });
3294
+ }
3295
+ /**
3296
+ * Delete an opportunity
3297
+ */
3298
+ async deleteOpportunity(opportunityId) {
3299
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
3300
+ method: 'DELETE',
3301
+ });
3302
+ }
3303
+ /**
3304
+ * Move opportunity to a different stage
3305
+ */
3306
+ async moveOpportunity(opportunityId, stageId) {
3307
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/move`, {
3308
+ method: 'POST',
3309
+ body: JSON.stringify({ stageId }),
3310
+ });
3311
+ }
3312
+ // ============================================
3313
+ // COMPANIES API
3314
+ // ============================================
3315
+ /**
3316
+ * Get all companies with pagination
3317
+ */
3318
+ async getCompanies(params) {
3319
+ const queryParams = new URLSearchParams();
3320
+ if (params?.page)
3321
+ queryParams.set('page', params.page.toString());
3322
+ if (params?.limit)
3323
+ queryParams.set('limit', params.limit.toString());
3324
+ if (params?.search)
3325
+ queryParams.set('search', params.search);
3326
+ if (params?.status)
3327
+ queryParams.set('status', params.status);
3328
+ if (params?.industry)
3329
+ queryParams.set('industry', params.industry);
3330
+ const query = queryParams.toString();
3331
+ const endpoint = `/api/workspaces/${this.workspaceId}/companies${query ? `?${query}` : ''}`;
3332
+ return this.request(endpoint);
3333
+ }
3334
+ /**
3335
+ * Get a single company by ID
3336
+ */
3337
+ async getCompany(companyId) {
3338
+ return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`);
3339
+ }
3340
+ /**
3341
+ * Create a new company
3342
+ */
3343
+ async createCompany(company) {
3344
+ return this.request(`/api/workspaces/${this.workspaceId}/companies`, {
3345
+ method: 'POST',
3346
+ body: JSON.stringify(company),
3347
+ });
3348
+ }
3349
+ /**
3350
+ * Update an existing company
3351
+ */
3352
+ async updateCompany(companyId, updates) {
3353
+ return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
3354
+ method: 'PUT',
3355
+ body: JSON.stringify(updates),
3356
+ });
3357
+ }
3358
+ /**
3359
+ * Delete a company
3360
+ */
3361
+ async deleteCompany(companyId) {
3362
+ return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
3363
+ method: 'DELETE',
3364
+ });
3365
+ }
3366
+ /**
3367
+ * Get contacts belonging to a company
3368
+ */
3369
+ async getCompanyContacts(companyId, params) {
3370
+ const queryParams = new URLSearchParams();
3371
+ if (params?.page)
3372
+ queryParams.set('page', params.page.toString());
3373
+ if (params?.limit)
3374
+ queryParams.set('limit', params.limit.toString());
3375
+ const query = queryParams.toString();
3376
+ const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/contacts${query ? `?${query}` : ''}`;
3377
+ return this.request(endpoint);
3378
+ }
3379
+ /**
3380
+ * Get deals/opportunities belonging to a company
3381
+ */
3382
+ async getCompanyDeals(companyId, params) {
3383
+ const queryParams = new URLSearchParams();
3384
+ if (params?.page)
3385
+ queryParams.set('page', params.page.toString());
3386
+ if (params?.limit)
3387
+ queryParams.set('limit', params.limit.toString());
3388
+ const query = queryParams.toString();
3389
+ const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/deals${query ? `?${query}` : ''}`;
3390
+ return this.request(endpoint);
3391
+ }
3392
+ // ============================================
3393
+ // PIPELINES API
3394
+ // ============================================
3395
+ /**
3396
+ * Get all pipelines
3397
+ */
3398
+ async getPipelines() {
3399
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines`);
3400
+ }
3401
+ /**
3402
+ * Get a single pipeline by ID
3403
+ */
3404
+ async getPipeline(pipelineId) {
3405
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`);
3406
+ }
3407
+ /**
3408
+ * Create a new pipeline
3409
+ */
3410
+ async createPipeline(pipeline) {
3411
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines`, {
3412
+ method: 'POST',
3413
+ body: JSON.stringify(pipeline),
3414
+ });
3415
+ }
3416
+ /**
3417
+ * Update an existing pipeline
3418
+ */
3419
+ async updatePipeline(pipelineId, updates) {
3420
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
3421
+ method: 'PUT',
3422
+ body: JSON.stringify(updates),
3423
+ });
3424
+ }
3425
+ /**
3426
+ * Delete a pipeline
3427
+ */
3428
+ async deletePipeline(pipelineId) {
3429
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
3430
+ method: 'DELETE',
3431
+ });
3432
+ }
3433
+ // ============================================
3434
+ // TASKS API
3435
+ // ============================================
3436
+ /**
3437
+ * Get all tasks with pagination
3438
+ */
3439
+ async getTasks(params) {
3440
+ const queryParams = new URLSearchParams();
3441
+ if (params?.page)
3442
+ queryParams.set('page', params.page.toString());
3443
+ if (params?.limit)
3444
+ queryParams.set('limit', params.limit.toString());
3445
+ if (params?.status)
3446
+ queryParams.set('status', params.status);
3447
+ if (params?.priority)
3448
+ queryParams.set('priority', params.priority);
3449
+ if (params?.contactId)
3450
+ queryParams.set('contactId', params.contactId);
3451
+ if (params?.companyId)
3452
+ queryParams.set('companyId', params.companyId);
3453
+ if (params?.opportunityId)
3454
+ queryParams.set('opportunityId', params.opportunityId);
3455
+ const query = queryParams.toString();
3456
+ const endpoint = `/api/workspaces/${this.workspaceId}/tasks${query ? `?${query}` : ''}`;
3457
+ return this.request(endpoint);
3458
+ }
3459
+ /**
3460
+ * Get a single task by ID
3461
+ */
3462
+ async getTask(taskId) {
3463
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`);
3464
+ }
3465
+ /**
3466
+ * Create a new task
3467
+ */
3468
+ async createTask(task) {
3469
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
3470
+ method: 'POST',
3471
+ body: JSON.stringify(task),
3472
+ });
3473
+ }
3474
+ /**
3475
+ * Update an existing task
3476
+ */
3477
+ async updateTask(taskId, updates) {
3478
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
3479
+ method: 'PUT',
3480
+ body: JSON.stringify(updates),
3481
+ });
3482
+ }
3483
+ /**
3484
+ * Mark a task as completed
3485
+ */
3486
+ async completeTask(taskId) {
3487
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}/complete`, {
3488
+ method: 'PATCH',
3489
+ });
3490
+ }
3491
+ /**
3492
+ * Delete a task
3493
+ */
3494
+ async deleteTask(taskId) {
3495
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
3496
+ method: 'DELETE',
3497
+ });
3498
+ }
3499
+ // ============================================
3500
+ // ACTIVITIES API
3501
+ // ============================================
3502
+ /**
3503
+ * Get activities for a contact
3504
+ */
3505
+ async getContactActivities(contactId, params) {
3506
+ const queryParams = new URLSearchParams();
3507
+ if (params?.page)
3508
+ queryParams.set('page', params.page.toString());
3509
+ if (params?.limit)
3510
+ queryParams.set('limit', params.limit.toString());
3511
+ if (params?.type)
3512
+ queryParams.set('type', params.type);
3513
+ const query = queryParams.toString();
3514
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
3515
+ return this.request(endpoint);
3516
+ }
3517
+ /**
3518
+ * Get activities for an opportunity/deal
3519
+ */
3520
+ async getOpportunityActivities(opportunityId, params) {
3521
+ const queryParams = new URLSearchParams();
3522
+ if (params?.page)
3523
+ queryParams.set('page', params.page.toString());
3524
+ if (params?.limit)
3525
+ queryParams.set('limit', params.limit.toString());
3526
+ if (params?.type)
3527
+ queryParams.set('type', params.type);
3528
+ const query = queryParams.toString();
3529
+ const endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/activities${query ? `?${query}` : ''}`;
3530
+ return this.request(endpoint);
3531
+ }
3532
+ /**
3533
+ * Create a new activity
3534
+ */
3535
+ async createActivity(activity) {
3536
+ // Determine the correct endpoint based on related entity
3537
+ let endpoint;
3538
+ if (activity.opportunityId) {
3539
+ endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${activity.opportunityId}/activities`;
3540
+ }
3541
+ else if (activity.contactId) {
3542
+ endpoint = `/api/workspaces/${this.workspaceId}/contacts/${activity.contactId}/activities`;
3543
+ }
3544
+ else {
3545
+ endpoint = `/api/workspaces/${this.workspaceId}/activities`;
3546
+ }
3547
+ return this.request(endpoint, {
3548
+ method: 'POST',
3549
+ body: JSON.stringify(activity),
3550
+ });
3551
+ }
3552
+ /**
3553
+ * Update an existing activity
3554
+ */
3555
+ async updateActivity(activityId, updates) {
3556
+ return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
3557
+ method: 'PATCH',
3558
+ body: JSON.stringify(updates),
3559
+ });
3560
+ }
3561
+ /**
3562
+ * Delete an activity
3563
+ */
3564
+ async deleteActivity(activityId) {
3565
+ return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
3566
+ method: 'DELETE',
3567
+ });
3568
+ }
3569
+ /**
3570
+ * Log a call activity
3571
+ */
3572
+ async logCall(data) {
3573
+ return this.createActivity({
3574
+ type: 'call',
3575
+ title: `${data.direction === 'inbound' ? 'Inbound' : 'Outbound'} Call`,
3576
+ direction: data.direction,
3577
+ duration: data.duration,
3578
+ outcome: data.outcome,
3579
+ description: data.notes,
3580
+ contactId: data.contactId,
3581
+ opportunityId: data.opportunityId,
3582
+ });
3583
+ }
3584
+ /**
3585
+ * Log a meeting activity
3586
+ */
3587
+ async logMeeting(data) {
3588
+ return this.createActivity({
3589
+ type: 'meeting',
3590
+ title: data.title,
3591
+ duration: data.duration,
3592
+ outcome: data.outcome,
3593
+ description: data.notes,
3594
+ contactId: data.contactId,
3595
+ opportunityId: data.opportunityId,
3596
+ });
3597
+ }
3598
+ /**
3599
+ * Add a note to a contact or opportunity
3600
+ */
3601
+ async addNote(data) {
3602
+ return this.createActivity({
3603
+ type: 'note',
3604
+ title: 'Note',
3605
+ description: data.content,
3606
+ contactId: data.contactId,
3607
+ opportunityId: data.opportunityId,
3608
+ });
3609
+ }
3610
+ // ============================================
3611
+ // EMAIL TEMPLATES API
3612
+ // ============================================
3613
+ /**
3614
+ * Get all email templates
3615
+ */
3616
+ async getEmailTemplates(params) {
3617
+ const queryParams = new URLSearchParams();
3618
+ if (params?.page)
3619
+ queryParams.set('page', params.page.toString());
3620
+ if (params?.limit)
3621
+ queryParams.set('limit', params.limit.toString());
3622
+ const query = queryParams.toString();
3623
+ const endpoint = `/api/workspaces/${this.workspaceId}/email-templates${query ? `?${query}` : ''}`;
3624
+ return this.request(endpoint);
3625
+ }
3626
+ /**
3627
+ * Get a single email template by ID
3628
+ */
3629
+ async getEmailTemplate(templateId) {
3630
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`);
3631
+ }
3632
+ /**
3633
+ * Create a new email template
3634
+ */
3635
+ async createEmailTemplate(template) {
3636
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates`, {
3637
+ method: 'POST',
3638
+ body: JSON.stringify(template),
3639
+ });
3640
+ }
3641
+ /**
3642
+ * Update an email template
3643
+ */
3644
+ async updateEmailTemplate(templateId, updates) {
3645
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
3646
+ method: 'PUT',
3647
+ body: JSON.stringify(updates),
3648
+ });
3649
+ }
3650
+ /**
3651
+ * Delete an email template
3652
+ */
3653
+ async deleteEmailTemplate(templateId) {
3654
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
3655
+ method: 'DELETE',
3656
+ });
3657
+ }
3658
+ /**
3659
+ * Send an email using a template
3660
+ */
3661
+ async sendEmail(data) {
3662
+ return this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
3663
+ method: 'POST',
3664
+ body: JSON.stringify(data),
3665
+ });
3666
+ }
3667
+ // ============================================
3668
+ // EVENT TRIGGERS API (delegated to triggers manager)
3669
+ // ============================================
3670
+ /**
3671
+ * Get all event triggers
3672
+ */
3673
+ async getEventTriggers() {
3674
+ return this.triggers.getTriggers();
3675
+ }
3676
+ /**
3677
+ * Create a new event trigger
3678
+ */
3679
+ async createEventTrigger(trigger) {
3680
+ return this.triggers.createTrigger(trigger);
3681
+ }
3682
+ /**
3683
+ * Update an event trigger
3684
+ */
3685
+ async updateEventTrigger(triggerId, updates) {
3686
+ return this.triggers.updateTrigger(triggerId, updates);
3687
+ }
3688
+ /**
3689
+ * Delete an event trigger
3690
+ */
3691
+ async deleteEventTrigger(triggerId) {
3692
+ return this.triggers.deleteTrigger(triggerId);
3693
+ }
3694
+ }
3695
+
2530
3696
  /**
2531
3697
  * Clianta SDK
2532
3698
  * Professional CRM and tracking SDK for lead generation
@@ -2579,11 +3745,13 @@
2579
3745
  Tracker,
2580
3746
  CRMClient,
2581
3747
  ConsentManager,
3748
+ EventTriggersManager,
2582
3749
  };
2583
3750
  }
2584
3751
 
2585
3752
  exports.CRMClient = CRMClient;
2586
3753
  exports.ConsentManager = ConsentManager;
3754
+ exports.EventTriggersManager = EventTriggersManager;
2587
3755
  exports.SDK_VERSION = SDK_VERSION;
2588
3756
  exports.Tracker = Tracker;
2589
3757
  exports.clianta = clianta;