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