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