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