@clianta/sdk 1.3.0 → 1.5.0

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