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