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