@clianta/sdk 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vue.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Clianta SDK v1.4.0
2
+ * Clianta SDK v1.5.0
3
3
  * (c) 2026 Clianta
4
4
  * Released under the MIT License.
5
5
  */
@@ -52,6 +52,7 @@ const DEFAULT_CONFIG = {
52
52
  cookieDomain: '',
53
53
  useCookies: false,
54
54
  cookielessMode: false,
55
+ persistMode: 'session',
55
56
  };
56
57
  /** Storage keys */
57
58
  const STORAGE_KEYS = {
@@ -245,6 +246,39 @@ class Transport {
245
246
  return false;
246
247
  }
247
248
  }
249
+ /**
250
+ * Fetch data from the tracking API (GET request)
251
+ * Used for read-back APIs (visitor profile, activity, etc.)
252
+ */
253
+ async fetchData(path, params) {
254
+ const url = new URL(`${this.config.apiEndpoint}${path}`);
255
+ if (params) {
256
+ Object.entries(params).forEach(([key, value]) => {
257
+ if (value !== undefined && value !== null) {
258
+ url.searchParams.set(key, value);
259
+ }
260
+ });
261
+ }
262
+ try {
263
+ const response = await this.fetchWithTimeout(url.toString(), {
264
+ method: 'GET',
265
+ headers: {
266
+ 'Accept': 'application/json',
267
+ },
268
+ });
269
+ if (response.ok) {
270
+ const body = await response.json();
271
+ logger.debug('Fetch successful:', path);
272
+ return { success: true, data: body.data ?? body, status: response.status };
273
+ }
274
+ logger.error(`Fetch failed with status ${response.status}`);
275
+ return { success: false, status: response.status };
276
+ }
277
+ catch (error) {
278
+ logger.error('Fetch request failed:', error);
279
+ return { success: false, error: error };
280
+ }
281
+ }
248
282
  /**
249
283
  * Internal send with retry logic
250
284
  */
@@ -409,7 +443,9 @@ function cookie(name, value, days) {
409
443
  date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
410
444
  expires = '; expires=' + date.toUTCString();
411
445
  }
412
- document.cookie = name + '=' + value + expires + '; path=/; SameSite=Lax';
446
+ // Add Secure flag on HTTPS to prevent cookie leakage over plaintext
447
+ const secure = typeof location !== 'undefined' && location.protocol === 'https:' ? '; Secure' : '';
448
+ document.cookie = name + '=' + value + expires + '; path=/; SameSite=Lax' + secure;
413
449
  return value;
414
450
  }
415
451
  // ============================================
@@ -573,6 +609,17 @@ function isMobile() {
573
609
  return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
574
610
  }
575
611
  // ============================================
612
+ // VALIDATION UTILITIES
613
+ // ============================================
614
+ /**
615
+ * Validate email format
616
+ */
617
+ function isValidEmail(email) {
618
+ if (typeof email !== 'string' || !email)
619
+ return false;
620
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
621
+ }
622
+ // ============================================
576
623
  // DEVICE INFO
577
624
  // ============================================
578
625
  /**
@@ -626,6 +673,7 @@ class EventQueue {
626
673
  maxQueueSize: config.maxQueueSize ?? MAX_QUEUE_SIZE,
627
674
  storageKey: config.storageKey ?? STORAGE_KEYS.EVENT_QUEUE,
628
675
  };
676
+ this.persistMode = config.persistMode || 'session';
629
677
  // Restore persisted queue
630
678
  this.restoreQueue();
631
679
  // Start auto-flush timer
@@ -731,6 +779,13 @@ class EventQueue {
731
779
  clear() {
732
780
  this.queue = [];
733
781
  this.persistQueue([]);
782
+ // Also clear localStorage if used
783
+ if (this.persistMode === 'local' && typeof localStorage !== 'undefined') {
784
+ try {
785
+ localStorage.removeItem(this.config.storageKey);
786
+ }
787
+ catch { /* ignore */ }
788
+ }
734
789
  }
735
790
  /**
736
791
  * Stop the flush timer and cleanup handlers
@@ -785,22 +840,44 @@ class EventQueue {
785
840
  window.addEventListener('pagehide', this.boundPageHide);
786
841
  }
787
842
  /**
788
- * Persist queue to localStorage
843
+ * Persist queue to storage based on persistMode
789
844
  */
790
845
  persistQueue(events) {
846
+ if (this.persistMode === 'none')
847
+ return;
791
848
  try {
792
- setLocalStorage(this.config.storageKey, JSON.stringify(events));
849
+ const serialized = JSON.stringify(events);
850
+ if (this.persistMode === 'local' && typeof localStorage !== 'undefined') {
851
+ try {
852
+ localStorage.setItem(this.config.storageKey, serialized);
853
+ }
854
+ catch {
855
+ // localStorage quota exceeded — fallback to sessionStorage
856
+ setSessionStorage(this.config.storageKey, serialized);
857
+ }
858
+ }
859
+ else {
860
+ setSessionStorage(this.config.storageKey, serialized);
861
+ }
793
862
  }
794
863
  catch {
795
864
  // Ignore storage errors
796
865
  }
797
866
  }
798
867
  /**
799
- * Restore queue from localStorage
868
+ * Restore queue from storage
800
869
  */
801
870
  restoreQueue() {
802
871
  try {
803
- const stored = getLocalStorage(this.config.storageKey);
872
+ let stored = null;
873
+ // Check localStorage first (cross-session persistence)
874
+ if (this.persistMode === 'local' && typeof localStorage !== 'undefined') {
875
+ stored = localStorage.getItem(this.config.storageKey);
876
+ }
877
+ // Fall back to sessionStorage
878
+ if (!stored) {
879
+ stored = getSessionStorage(this.config.storageKey);
880
+ }
804
881
  if (stored) {
805
882
  const events = JSON.parse(stored);
806
883
  if (Array.isArray(events) && events.length > 0) {
@@ -868,10 +945,13 @@ class PageViewPlugin extends BasePlugin {
868
945
  history.pushState = function (...args) {
869
946
  self.originalPushState.apply(history, args);
870
947
  self.trackPageView();
948
+ // Notify other plugins (e.g. ScrollPlugin) about navigation
949
+ window.dispatchEvent(new Event('clianta:navigation'));
871
950
  };
872
951
  history.replaceState = function (...args) {
873
952
  self.originalReplaceState.apply(history, args);
874
953
  self.trackPageView();
954
+ window.dispatchEvent(new Event('clianta:navigation'));
875
955
  };
876
956
  // Handle back/forward navigation
877
957
  this.popstateHandler = () => this.trackPageView();
@@ -925,9 +1005,8 @@ class ScrollPlugin extends BasePlugin {
925
1005
  this.pageLoadTime = 0;
926
1006
  this.scrollTimeout = null;
927
1007
  this.boundHandler = null;
928
- /** SPA navigation support */
929
- this.originalPushState = null;
930
- this.originalReplaceState = null;
1008
+ /** SPA navigation listen for PageViewPlugin's custom event instead of patching history */
1009
+ this.navigationHandler = null;
931
1010
  this.popstateHandler = null;
932
1011
  }
933
1012
  init(tracker) {
@@ -936,8 +1015,13 @@ class ScrollPlugin extends BasePlugin {
936
1015
  if (typeof window !== 'undefined') {
937
1016
  this.boundHandler = this.handleScroll.bind(this);
938
1017
  window.addEventListener('scroll', this.boundHandler, { passive: true });
939
- // Setup SPA navigation reset
940
- this.setupNavigationReset();
1018
+ // Listen for navigation events dispatched by PageViewPlugin
1019
+ // instead of independently monkey-patching history.pushState
1020
+ this.navigationHandler = () => this.resetForNavigation();
1021
+ window.addEventListener('clianta:navigation', this.navigationHandler);
1022
+ // Handle back/forward navigation
1023
+ this.popstateHandler = () => this.resetForNavigation();
1024
+ window.addEventListener('popstate', this.popstateHandler);
941
1025
  }
942
1026
  }
943
1027
  destroy() {
@@ -947,16 +1031,10 @@ class ScrollPlugin extends BasePlugin {
947
1031
  if (this.scrollTimeout) {
948
1032
  clearTimeout(this.scrollTimeout);
949
1033
  }
950
- // Restore original history methods
951
- if (this.originalPushState) {
952
- history.pushState = this.originalPushState;
953
- this.originalPushState = null;
1034
+ if (this.navigationHandler && typeof window !== 'undefined') {
1035
+ window.removeEventListener('clianta:navigation', this.navigationHandler);
1036
+ this.navigationHandler = null;
954
1037
  }
955
- if (this.originalReplaceState) {
956
- history.replaceState = this.originalReplaceState;
957
- this.originalReplaceState = null;
958
- }
959
- // Remove popstate listener
960
1038
  if (this.popstateHandler && typeof window !== 'undefined') {
961
1039
  window.removeEventListener('popstate', this.popstateHandler);
962
1040
  this.popstateHandler = null;
@@ -971,29 +1049,6 @@ class ScrollPlugin extends BasePlugin {
971
1049
  this.maxScrollDepth = 0;
972
1050
  this.pageLoadTime = Date.now();
973
1051
  }
974
- /**
975
- * Setup History API interception for SPA navigation
976
- */
977
- setupNavigationReset() {
978
- if (typeof window === 'undefined')
979
- return;
980
- // Store originals for cleanup
981
- this.originalPushState = history.pushState;
982
- this.originalReplaceState = history.replaceState;
983
- // Intercept pushState and replaceState
984
- const self = this;
985
- history.pushState = function (...args) {
986
- self.originalPushState.apply(history, args);
987
- self.resetForNavigation();
988
- };
989
- history.replaceState = function (...args) {
990
- self.originalReplaceState.apply(history, args);
991
- self.resetForNavigation();
992
- };
993
- // Handle back/forward navigation
994
- this.popstateHandler = () => this.resetForNavigation();
995
- window.addEventListener('popstate', this.popstateHandler);
996
- }
997
1052
  handleScroll() {
998
1053
  // Debounce scroll tracking
999
1054
  if (this.scrollTimeout) {
@@ -1193,6 +1248,10 @@ class ClicksPlugin extends BasePlugin {
1193
1248
  elementId: elementInfo.id,
1194
1249
  elementClass: elementInfo.className,
1195
1250
  href: target.href || undefined,
1251
+ x: Math.round((e.clientX / window.innerWidth) * 100),
1252
+ y: Math.round((e.clientY / window.innerHeight) * 100),
1253
+ viewportWidth: window.innerWidth,
1254
+ viewportHeight: window.innerHeight,
1196
1255
  });
1197
1256
  }
1198
1257
  }
@@ -1215,6 +1274,9 @@ class EngagementPlugin extends BasePlugin {
1215
1274
  this.boundMarkEngaged = null;
1216
1275
  this.boundTrackTimeOnPage = null;
1217
1276
  this.boundVisibilityHandler = null;
1277
+ /** SPA navigation — listen for PageViewPlugin's custom event instead of patching history */
1278
+ this.navigationHandler = null;
1279
+ this.popstateHandler = null;
1218
1280
  }
1219
1281
  init(tracker) {
1220
1282
  super.init(tracker);
@@ -1240,6 +1302,13 @@ class EngagementPlugin extends BasePlugin {
1240
1302
  // Track time on page before unload
1241
1303
  window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
1242
1304
  document.addEventListener('visibilitychange', this.boundVisibilityHandler);
1305
+ // Listen for navigation events dispatched by PageViewPlugin
1306
+ // instead of independently monkey-patching history.pushState
1307
+ this.navigationHandler = () => this.resetForNavigation();
1308
+ window.addEventListener('clianta:navigation', this.navigationHandler);
1309
+ // Handle back/forward navigation
1310
+ this.popstateHandler = () => this.resetForNavigation();
1311
+ window.addEventListener('popstate', this.popstateHandler);
1243
1312
  }
1244
1313
  destroy() {
1245
1314
  if (this.boundMarkEngaged && typeof document !== 'undefined') {
@@ -1253,11 +1322,28 @@ class EngagementPlugin extends BasePlugin {
1253
1322
  if (this.boundVisibilityHandler && typeof document !== 'undefined') {
1254
1323
  document.removeEventListener('visibilitychange', this.boundVisibilityHandler);
1255
1324
  }
1325
+ if (this.navigationHandler && typeof window !== 'undefined') {
1326
+ window.removeEventListener('clianta:navigation', this.navigationHandler);
1327
+ this.navigationHandler = null;
1328
+ }
1329
+ if (this.popstateHandler && typeof window !== 'undefined') {
1330
+ window.removeEventListener('popstate', this.popstateHandler);
1331
+ this.popstateHandler = null;
1332
+ }
1256
1333
  if (this.engagementTimeout) {
1257
1334
  clearTimeout(this.engagementTimeout);
1258
1335
  }
1259
1336
  super.destroy();
1260
1337
  }
1338
+ resetForNavigation() {
1339
+ this.pageLoadTime = Date.now();
1340
+ this.engagementStartTime = Date.now();
1341
+ this.isEngaged = false;
1342
+ if (this.engagementTimeout) {
1343
+ clearTimeout(this.engagementTimeout);
1344
+ this.engagementTimeout = null;
1345
+ }
1346
+ }
1261
1347
  markEngaged() {
1262
1348
  if (!this.isEngaged) {
1263
1349
  this.isEngaged = true;
@@ -1297,9 +1383,8 @@ class DownloadsPlugin extends BasePlugin {
1297
1383
  this.name = 'downloads';
1298
1384
  this.trackedDownloads = new Set();
1299
1385
  this.boundHandler = null;
1300
- /** SPA navigation support */
1301
- this.originalPushState = null;
1302
- this.originalReplaceState = null;
1386
+ /** SPA navigation listen for PageViewPlugin's custom event instead of patching history */
1387
+ this.navigationHandler = null;
1303
1388
  this.popstateHandler = null;
1304
1389
  }
1305
1390
  init(tracker) {
@@ -1307,24 +1392,25 @@ class DownloadsPlugin extends BasePlugin {
1307
1392
  if (typeof document !== 'undefined') {
1308
1393
  this.boundHandler = this.handleClick.bind(this);
1309
1394
  document.addEventListener('click', this.boundHandler, true);
1310
- // Setup SPA navigation reset
1311
- this.setupNavigationReset();
1395
+ }
1396
+ if (typeof window !== 'undefined') {
1397
+ // Listen for navigation events dispatched by PageViewPlugin
1398
+ // instead of independently monkey-patching history.pushState
1399
+ this.navigationHandler = () => this.resetForNavigation();
1400
+ window.addEventListener('clianta:navigation', this.navigationHandler);
1401
+ // Handle back/forward navigation
1402
+ this.popstateHandler = () => this.resetForNavigation();
1403
+ window.addEventListener('popstate', this.popstateHandler);
1312
1404
  }
1313
1405
  }
1314
1406
  destroy() {
1315
1407
  if (this.boundHandler && typeof document !== 'undefined') {
1316
1408
  document.removeEventListener('click', this.boundHandler, true);
1317
1409
  }
1318
- // Restore original history methods
1319
- if (this.originalPushState) {
1320
- history.pushState = this.originalPushState;
1321
- this.originalPushState = null;
1322
- }
1323
- if (this.originalReplaceState) {
1324
- history.replaceState = this.originalReplaceState;
1325
- this.originalReplaceState = null;
1410
+ if (this.navigationHandler && typeof window !== 'undefined') {
1411
+ window.removeEventListener('clianta:navigation', this.navigationHandler);
1412
+ this.navigationHandler = null;
1326
1413
  }
1327
- // Remove popstate listener
1328
1414
  if (this.popstateHandler && typeof window !== 'undefined') {
1329
1415
  window.removeEventListener('popstate', this.popstateHandler);
1330
1416
  this.popstateHandler = null;
@@ -1337,29 +1423,6 @@ class DownloadsPlugin extends BasePlugin {
1337
1423
  resetForNavigation() {
1338
1424
  this.trackedDownloads.clear();
1339
1425
  }
1340
- /**
1341
- * Setup History API interception for SPA navigation
1342
- */
1343
- setupNavigationReset() {
1344
- if (typeof window === 'undefined')
1345
- return;
1346
- // Store originals for cleanup
1347
- this.originalPushState = history.pushState;
1348
- this.originalReplaceState = history.replaceState;
1349
- // Intercept pushState and replaceState
1350
- const self = this;
1351
- history.pushState = function (...args) {
1352
- self.originalPushState.apply(history, args);
1353
- self.resetForNavigation();
1354
- };
1355
- history.replaceState = function (...args) {
1356
- self.originalReplaceState.apply(history, args);
1357
- self.resetForNavigation();
1358
- };
1359
- // Handle back/forward navigation
1360
- this.popstateHandler = () => this.resetForNavigation();
1361
- window.addEventListener('popstate', this.popstateHandler);
1362
- }
1363
1426
  handleClick(e) {
1364
1427
  const link = e.target.closest('a');
1365
1428
  if (!link || !link.href)
@@ -1395,6 +1458,9 @@ class ExitIntentPlugin extends BasePlugin {
1395
1458
  this.exitIntentShown = false;
1396
1459
  this.pageLoadTime = 0;
1397
1460
  this.boundHandler = null;
1461
+ /** SPA navigation — listen for PageViewPlugin's custom event instead of patching history */
1462
+ this.navigationHandler = null;
1463
+ this.popstateHandler = null;
1398
1464
  }
1399
1465
  init(tracker) {
1400
1466
  super.init(tracker);
@@ -1406,13 +1472,34 @@ class ExitIntentPlugin extends BasePlugin {
1406
1472
  this.boundHandler = this.handleMouseLeave.bind(this);
1407
1473
  document.addEventListener('mouseleave', this.boundHandler);
1408
1474
  }
1475
+ if (typeof window !== 'undefined') {
1476
+ // Listen for navigation events dispatched by PageViewPlugin
1477
+ // instead of independently monkey-patching history.pushState
1478
+ this.navigationHandler = () => this.resetForNavigation();
1479
+ window.addEventListener('clianta:navigation', this.navigationHandler);
1480
+ // Handle back/forward navigation
1481
+ this.popstateHandler = () => this.resetForNavigation();
1482
+ window.addEventListener('popstate', this.popstateHandler);
1483
+ }
1409
1484
  }
1410
1485
  destroy() {
1411
1486
  if (this.boundHandler && typeof document !== 'undefined') {
1412
1487
  document.removeEventListener('mouseleave', this.boundHandler);
1413
1488
  }
1489
+ if (this.navigationHandler && typeof window !== 'undefined') {
1490
+ window.removeEventListener('clianta:navigation', this.navigationHandler);
1491
+ this.navigationHandler = null;
1492
+ }
1493
+ if (this.popstateHandler && typeof window !== 'undefined') {
1494
+ window.removeEventListener('popstate', this.popstateHandler);
1495
+ this.popstateHandler = null;
1496
+ }
1414
1497
  super.destroy();
1415
1498
  }
1499
+ resetForNavigation() {
1500
+ this.exitIntentShown = false;
1501
+ this.pageLoadTime = Date.now();
1502
+ }
1416
1503
  handleMouseLeave(e) {
1417
1504
  // Only trigger when mouse leaves from the top of the page
1418
1505
  if (e.clientY > 0 || this.exitIntentShown)
@@ -1640,6 +1727,8 @@ class PopupFormsPlugin extends BasePlugin {
1640
1727
  this.shownForms = new Set();
1641
1728
  this.scrollHandler = null;
1642
1729
  this.exitHandler = null;
1730
+ this.delayTimers = [];
1731
+ this.clickTriggerListeners = [];
1643
1732
  }
1644
1733
  async init(tracker) {
1645
1734
  super.init(tracker);
@@ -1654,6 +1743,14 @@ class PopupFormsPlugin extends BasePlugin {
1654
1743
  }
1655
1744
  destroy() {
1656
1745
  this.removeTriggers();
1746
+ for (const timer of this.delayTimers) {
1747
+ clearTimeout(timer);
1748
+ }
1749
+ this.delayTimers = [];
1750
+ for (const { element, handler } of this.clickTriggerListeners) {
1751
+ element.removeEventListener('click', handler);
1752
+ }
1753
+ this.clickTriggerListeners = [];
1657
1754
  super.destroy();
1658
1755
  }
1659
1756
  loadShownForms() {
@@ -1716,7 +1813,7 @@ class PopupFormsPlugin extends BasePlugin {
1716
1813
  this.forms.forEach(form => {
1717
1814
  switch (form.trigger.type) {
1718
1815
  case 'delay':
1719
- setTimeout(() => this.showForm(form), (form.trigger.value || 5) * 1000);
1816
+ this.delayTimers.push(setTimeout(() => this.showForm(form), (form.trigger.value || 5) * 1000));
1720
1817
  break;
1721
1818
  case 'scroll':
1722
1819
  this.setupScrollTrigger(form);
@@ -1759,7 +1856,9 @@ class PopupFormsPlugin extends BasePlugin {
1759
1856
  return;
1760
1857
  const elements = document.querySelectorAll(form.trigger.selector);
1761
1858
  elements.forEach(el => {
1762
- el.addEventListener('click', () => this.showForm(form));
1859
+ const handler = () => this.showForm(form);
1860
+ el.addEventListener('click', handler);
1861
+ this.clickTriggerListeners.push({ element: el, handler });
1763
1862
  });
1764
1863
  }
1765
1864
  removeTriggers() {
@@ -2046,7 +2145,7 @@ class PopupFormsPlugin extends BasePlugin {
2046
2145
  const submitBtn = formElement.querySelector('button[type="submit"]');
2047
2146
  if (submitBtn) {
2048
2147
  submitBtn.disabled = true;
2049
- submitBtn.innerHTML = 'Submitting...';
2148
+ submitBtn.textContent = 'Submitting...';
2050
2149
  }
2051
2150
  try {
2052
2151
  const response = await fetch(`${apiEndpoint}/api/public/lead-forms/${form._id}/submit`, {
@@ -2087,11 +2186,24 @@ class PopupFormsPlugin extends BasePlugin {
2087
2186
  if (data.email) {
2088
2187
  this.tracker?.identify(data.email, data);
2089
2188
  }
2090
- // Redirect if configured
2189
+ // Redirect if configured (validate URL to prevent open redirect)
2091
2190
  if (form.redirectUrl) {
2092
- setTimeout(() => {
2093
- window.location.href = form.redirectUrl;
2094
- }, 1500);
2191
+ try {
2192
+ const redirect = new URL(form.redirectUrl, window.location.origin);
2193
+ const isSameOrigin = redirect.origin === window.location.origin;
2194
+ const isSafeProtocol = redirect.protocol === 'https:' || redirect.protocol === 'http:';
2195
+ if (isSameOrigin || isSafeProtocol) {
2196
+ setTimeout(() => {
2197
+ window.location.href = redirect.href;
2198
+ }, 1500);
2199
+ }
2200
+ else {
2201
+ console.warn('[Clianta] Blocked unsafe redirect URL:', form.redirectUrl);
2202
+ }
2203
+ }
2204
+ catch {
2205
+ console.warn('[Clianta] Invalid redirect URL:', form.redirectUrl);
2206
+ }
2095
2207
  }
2096
2208
  // Close after delay
2097
2209
  setTimeout(() => {
@@ -2106,7 +2218,7 @@ class PopupFormsPlugin extends BasePlugin {
2106
2218
  console.error('[Clianta] Form submit error:', error);
2107
2219
  if (submitBtn) {
2108
2220
  submitBtn.disabled = false;
2109
- submitBtn.innerHTML = form.submitButtonText || 'Subscribe';
2221
+ submitBtn.textContent = form.submitButtonText || 'Subscribe';
2110
2222
  }
2111
2223
  }
2112
2224
  }
@@ -3437,6 +3549,114 @@ class CRMClient {
3437
3549
  });
3438
3550
  }
3439
3551
  // ============================================
3552
+ // READ-BACK / DATA RETRIEVAL API
3553
+ // ============================================
3554
+ /**
3555
+ * Get a contact by email address.
3556
+ * Returns the first matching contact from a search query.
3557
+ */
3558
+ async getContactByEmail(email) {
3559
+ this.validateRequired('email', email, 'getContactByEmail');
3560
+ const queryParams = new URLSearchParams({ search: email, limit: '1' });
3561
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts?${queryParams.toString()}`);
3562
+ }
3563
+ /**
3564
+ * Get activity timeline for a contact
3565
+ */
3566
+ async getContactActivity(contactId, params) {
3567
+ this.validateRequired('contactId', contactId, 'getContactActivity');
3568
+ const queryParams = new URLSearchParams();
3569
+ if (params?.page)
3570
+ queryParams.set('page', params.page.toString());
3571
+ if (params?.limit)
3572
+ queryParams.set('limit', params.limit.toString());
3573
+ if (params?.type)
3574
+ queryParams.set('type', params.type);
3575
+ if (params?.startDate)
3576
+ queryParams.set('startDate', params.startDate);
3577
+ if (params?.endDate)
3578
+ queryParams.set('endDate', params.endDate);
3579
+ const query = queryParams.toString();
3580
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
3581
+ return this.request(endpoint);
3582
+ }
3583
+ /**
3584
+ * Get engagement metrics for a contact (via their linked visitor data)
3585
+ */
3586
+ async getContactEngagement(contactId) {
3587
+ this.validateRequired('contactId', contactId, 'getContactEngagement');
3588
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}/engagement`);
3589
+ }
3590
+ /**
3591
+ * Get a full timeline for a contact including events, activities, and opportunities
3592
+ */
3593
+ async getContactTimeline(contactId, params) {
3594
+ this.validateRequired('contactId', contactId, 'getContactTimeline');
3595
+ const queryParams = new URLSearchParams();
3596
+ if (params?.page)
3597
+ queryParams.set('page', params.page.toString());
3598
+ if (params?.limit)
3599
+ queryParams.set('limit', params.limit.toString());
3600
+ const query = queryParams.toString();
3601
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/timeline${query ? `?${query}` : ''}`;
3602
+ return this.request(endpoint);
3603
+ }
3604
+ /**
3605
+ * Search contacts with advanced filters
3606
+ */
3607
+ async searchContacts(query, filters) {
3608
+ const queryParams = new URLSearchParams();
3609
+ queryParams.set('search', query);
3610
+ if (filters?.status)
3611
+ queryParams.set('status', filters.status);
3612
+ if (filters?.lifecycleStage)
3613
+ queryParams.set('lifecycleStage', filters.lifecycleStage);
3614
+ if (filters?.source)
3615
+ queryParams.set('source', filters.source);
3616
+ if (filters?.tags)
3617
+ queryParams.set('tags', filters.tags.join(','));
3618
+ if (filters?.page)
3619
+ queryParams.set('page', filters.page.toString());
3620
+ if (filters?.limit)
3621
+ queryParams.set('limit', filters.limit.toString());
3622
+ const qs = queryParams.toString();
3623
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts${qs ? `?${qs}` : ''}`;
3624
+ return this.request(endpoint);
3625
+ }
3626
+ // ============================================
3627
+ // WEBHOOK MANAGEMENT API
3628
+ // ============================================
3629
+ /**
3630
+ * List all webhook subscriptions
3631
+ */
3632
+ async listWebhooks(params) {
3633
+ const queryParams = new URLSearchParams();
3634
+ if (params?.page)
3635
+ queryParams.set('page', params.page.toString());
3636
+ if (params?.limit)
3637
+ queryParams.set('limit', params.limit.toString());
3638
+ const query = queryParams.toString();
3639
+ return this.request(`/api/workspaces/${this.workspaceId}/webhooks${query ? `?${query}` : ''}`);
3640
+ }
3641
+ /**
3642
+ * Create a new webhook subscription
3643
+ */
3644
+ async createWebhook(data) {
3645
+ return this.request(`/api/workspaces/${this.workspaceId}/webhooks`, {
3646
+ method: 'POST',
3647
+ body: JSON.stringify(data),
3648
+ });
3649
+ }
3650
+ /**
3651
+ * Delete a webhook subscription
3652
+ */
3653
+ async deleteWebhook(webhookId) {
3654
+ this.validateRequired('webhookId', webhookId, 'deleteWebhook');
3655
+ return this.request(`/api/workspaces/${this.workspaceId}/webhooks/${webhookId}`, {
3656
+ method: 'DELETE',
3657
+ });
3658
+ }
3659
+ // ============================================
3440
3660
  // EVENT TRIGGERS API (delegated to triggers manager)
3441
3661
  // ============================================
3442
3662
  /**
@@ -3480,6 +3700,8 @@ class Tracker {
3480
3700
  this.contactId = null;
3481
3701
  /** Pending identify retry on next flush */
3482
3702
  this.pendingIdentify = null;
3703
+ /** Registered event schemas for validation */
3704
+ this.eventSchemas = new Map();
3483
3705
  if (!workspaceId) {
3484
3706
  throw new Error('[Clianta] Workspace ID is required');
3485
3707
  }
@@ -3505,6 +3727,16 @@ class Tracker {
3505
3727
  this.visitorId = this.createVisitorId();
3506
3728
  this.sessionId = this.createSessionId();
3507
3729
  logger.debug('IDs created', { visitorId: this.visitorId, sessionId: this.sessionId });
3730
+ // Security warnings
3731
+ if (this.config.apiEndpoint.startsWith('http://') &&
3732
+ typeof window !== 'undefined' &&
3733
+ !window.location.hostname.includes('localhost') &&
3734
+ !window.location.hostname.includes('127.0.0.1')) {
3735
+ logger.warn('apiEndpoint uses HTTP — events and visitor data will be sent unencrypted. Use HTTPS in production.');
3736
+ }
3737
+ if (this.config.apiKey && typeof window !== 'undefined') {
3738
+ logger.warn('API key is exposed in client-side code. Use API keys only in server-side (Node.js) environments.');
3739
+ }
3508
3740
  // Initialize plugins
3509
3741
  this.initPlugins();
3510
3742
  this.isInitialized = true;
@@ -3610,6 +3842,7 @@ class Tracker {
3610
3842
  properties: {
3611
3843
  ...properties,
3612
3844
  eventId: generateUUID(), // Unique ID for deduplication on retry
3845
+ websiteDomain: typeof window !== 'undefined' ? window.location.hostname : undefined,
3613
3846
  },
3614
3847
  device: getDeviceInfo(),
3615
3848
  ...getUTMParams(),
@@ -3620,6 +3853,8 @@ class Tracker {
3620
3853
  if (this.contactId) {
3621
3854
  event.contactId = this.contactId;
3622
3855
  }
3856
+ // Validate event against registered schema (debug mode only)
3857
+ this.validateEventSchema(eventType, properties);
3623
3858
  // Check consent before tracking
3624
3859
  if (!this.consentManager.canTrack()) {
3625
3860
  // Buffer event for later if waitForConsent is enabled
@@ -3654,6 +3889,10 @@ class Tracker {
3654
3889
  logger.warn('Email is required for identification');
3655
3890
  return null;
3656
3891
  }
3892
+ if (!isValidEmail(email)) {
3893
+ logger.warn('Invalid email format, identification skipped:', email);
3894
+ return null;
3895
+ }
3657
3896
  logger.info('Identifying visitor:', email);
3658
3897
  const result = await this.transport.sendIdentify({
3659
3898
  workspaceId: this.workspaceId,
@@ -3688,6 +3927,83 @@ class Tracker {
3688
3927
  const client = new CRMClient(this.config.apiEndpoint, this.workspaceId, undefined, apiKey);
3689
3928
  return client.sendEvent(payload);
3690
3929
  }
3930
+ /**
3931
+ * Get the current visitor's profile from the CRM.
3932
+ * Returns visitor data and linked contact info if identified.
3933
+ * Only returns data for the current visitor (privacy-safe for frontend).
3934
+ */
3935
+ async getVisitorProfile() {
3936
+ if (!this.isInitialized) {
3937
+ logger.warn('SDK not initialized');
3938
+ return null;
3939
+ }
3940
+ const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/profile`);
3941
+ if (result.success && result.data) {
3942
+ logger.debug('Visitor profile fetched:', result.data);
3943
+ return result.data;
3944
+ }
3945
+ logger.warn('Failed to fetch visitor profile:', result.error);
3946
+ return null;
3947
+ }
3948
+ /**
3949
+ * Get the current visitor's recent activity/events.
3950
+ * Returns paginated list of tracking events for this visitor.
3951
+ */
3952
+ async getVisitorActivity(options) {
3953
+ if (!this.isInitialized) {
3954
+ logger.warn('SDK not initialized');
3955
+ return null;
3956
+ }
3957
+ const params = {};
3958
+ if (options?.page)
3959
+ params.page = options.page.toString();
3960
+ if (options?.limit)
3961
+ params.limit = options.limit.toString();
3962
+ if (options?.eventType)
3963
+ params.eventType = options.eventType;
3964
+ if (options?.startDate)
3965
+ params.startDate = options.startDate;
3966
+ if (options?.endDate)
3967
+ params.endDate = options.endDate;
3968
+ const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/activity`, params);
3969
+ if (result.success && result.data) {
3970
+ return result.data;
3971
+ }
3972
+ logger.warn('Failed to fetch visitor activity:', result.error);
3973
+ return null;
3974
+ }
3975
+ /**
3976
+ * Get a summarized journey timeline for the current visitor.
3977
+ * Includes top pages, sessions, time spent, and recent activities.
3978
+ */
3979
+ async getVisitorTimeline() {
3980
+ if (!this.isInitialized) {
3981
+ logger.warn('SDK not initialized');
3982
+ return null;
3983
+ }
3984
+ const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/timeline`);
3985
+ if (result.success && result.data) {
3986
+ return result.data;
3987
+ }
3988
+ logger.warn('Failed to fetch visitor timeline:', result.error);
3989
+ return null;
3990
+ }
3991
+ /**
3992
+ * Get engagement metrics for the current visitor.
3993
+ * Includes time on site, page views, bounce rate, and engagement score.
3994
+ */
3995
+ async getVisitorEngagement() {
3996
+ if (!this.isInitialized) {
3997
+ logger.warn('SDK not initialized');
3998
+ return null;
3999
+ }
4000
+ const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/engagement`);
4001
+ if (result.success && result.data) {
4002
+ return result.data;
4003
+ }
4004
+ logger.warn('Failed to fetch visitor engagement:', result.error);
4005
+ return null;
4006
+ }
3691
4007
  /**
3692
4008
  * Retry pending identify call
3693
4009
  */
@@ -3717,6 +4033,59 @@ class Tracker {
3717
4033
  logger.enabled = enabled;
3718
4034
  logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
3719
4035
  }
4036
+ /**
4037
+ * Register a schema for event validation.
4038
+ * When debug mode is enabled, events will be validated against registered schemas.
4039
+ *
4040
+ * @example
4041
+ * tracker.registerEventSchema('purchase', {
4042
+ * productId: 'string',
4043
+ * price: 'number',
4044
+ * quantity: 'number',
4045
+ * });
4046
+ */
4047
+ registerEventSchema(eventType, schema) {
4048
+ this.eventSchemas.set(eventType, schema);
4049
+ logger.debug('Event schema registered:', eventType);
4050
+ }
4051
+ /**
4052
+ * Validate event properties against a registered schema (debug mode only)
4053
+ */
4054
+ validateEventSchema(eventType, properties) {
4055
+ if (!this.config.debug)
4056
+ return;
4057
+ const schema = this.eventSchemas.get(eventType);
4058
+ if (!schema)
4059
+ return;
4060
+ for (const [key, expectedType] of Object.entries(schema)) {
4061
+ const value = properties[key];
4062
+ if (value === undefined) {
4063
+ logger.warn(`[Schema] Missing property "${key}" for event type "${eventType}"`);
4064
+ continue;
4065
+ }
4066
+ let valid = false;
4067
+ switch (expectedType) {
4068
+ case 'string':
4069
+ valid = typeof value === 'string';
4070
+ break;
4071
+ case 'number':
4072
+ valid = typeof value === 'number';
4073
+ break;
4074
+ case 'boolean':
4075
+ valid = typeof value === 'boolean';
4076
+ break;
4077
+ case 'object':
4078
+ valid = typeof value === 'object' && !Array.isArray(value);
4079
+ break;
4080
+ case 'array':
4081
+ valid = Array.isArray(value);
4082
+ break;
4083
+ }
4084
+ if (!valid) {
4085
+ logger.warn(`[Schema] Property "${key}" for event "${eventType}" expected ${expectedType}, got ${typeof value}`);
4086
+ }
4087
+ }
4088
+ }
3720
4089
  /**
3721
4090
  * Get visitor ID
3722
4091
  */
@@ -3756,6 +4125,8 @@ class Tracker {
3756
4125
  resetIds(this.config.useCookies);
3757
4126
  this.visitorId = this.createVisitorId();
3758
4127
  this.sessionId = this.createSessionId();
4128
+ this.contactId = null;
4129
+ this.pendingIdentify = null;
3759
4130
  this.queue.clear();
3760
4131
  }
3761
4132
  /**