@athena-tracker/tracker 1.2.0 → 1.3.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/index.esm.js CHANGED
@@ -5,7 +5,7 @@ import { StyleSheet, View, ActivityIndicator, Text, AppState, PanResponder } fro
5
5
  * Auto-Detection Logic
6
6
  *
7
7
  * Automatically detects whether to use on-device or server-side inference
8
- * based on availability of onnxruntime-react-native
8
+ * based on availability of onnxruntime-web (browser) or onnxruntime-react-native (RN)
9
9
  */
10
10
  async function detectInferenceMode(configMode) {
11
11
  // Respect explicit configuration
@@ -17,14 +17,31 @@ async function detectInferenceMode(configMode) {
17
17
  console.log('[ATHENA] Forcing server-side inference mode');
18
18
  return 'server';
19
19
  }
20
- // Auto-detect based on onnxruntime-react-native availability
21
- try {
22
- // Try to dynamically import onnxruntime-react-native
23
- await import('onnxruntime-react-native');
24
- console.log('[ATHENA] On-device inference available (onnxruntime-react-native detected)');
25
- return 'on-device';
26
- } catch (error) {
27
- console.log('[ATHENA] Falling back to server-side inference (onnxruntime-react-native not found)');
20
+ // Auto-detect based on platform and ONNX runtime availability
21
+ const platform = getPlatform();
22
+ if (platform === 'web') {
23
+ // Try to load onnxruntime-web for browsers
24
+ try {
25
+ await import('onnxruntime-web');
26
+ console.log('[ATHENA] On-device inference available (onnxruntime-web detected)');
27
+ return 'on-device';
28
+ } catch (error) {
29
+ console.log('[ATHENA] Falling back to server-side inference (onnxruntime-web not found)');
30
+ return 'server';
31
+ }
32
+ } else if (platform === 'react-native') {
33
+ // Try to load onnxruntime-react-native for React Native
34
+ try {
35
+ await import('onnxruntime-react-native');
36
+ console.log('[ATHENA] On-device inference available (onnxruntime-react-native detected)');
37
+ return 'on-device';
38
+ } catch (error) {
39
+ console.log('[ATHENA] Falling back to server-side inference (onnxruntime-react-native not found)');
40
+ return 'server';
41
+ }
42
+ } else {
43
+ // Unknown platform - use server-side inference
44
+ console.log('[ATHENA] Falling back to server-side inference (unknown platform)');
28
45
  return 'server';
29
46
  }
30
47
  }
@@ -58,14 +75,14 @@ function getPlatform() {
58
75
  * Uses onnxruntime-react-native for local ML inference
59
76
  * Target latency: <10ms P95
60
77
  */
61
- let InferenceSession;
62
- let Tensor;
78
+ let InferenceSession$1;
79
+ let Tensor$1;
63
80
  // Dynamically import ONNX Runtime (only available in React Native)
64
81
  async function loadOnnxRuntime() {
65
82
  try {
66
83
  const onnx = await import('onnxruntime-react-native');
67
- InferenceSession = onnx.InferenceSession;
68
- Tensor = onnx.Tensor;
84
+ InferenceSession$1 = onnx.InferenceSession;
85
+ Tensor$1 = onnx.Tensor;
69
86
  return true;
70
87
  } catch (error) {
71
88
  console.error('[ATHENA] Failed to load onnxruntime-react-native:', error);
@@ -89,7 +106,7 @@ class OnDeviceInference {
89
106
  }
90
107
  try {
91
108
  const startTime = performance.now();
92
- this.session = await InferenceSession.create(modelPath);
109
+ this.session = await InferenceSession$1.create(modelPath);
93
110
  const loadTime = performance.now() - startTime;
94
111
  this.modelLoaded = true;
95
112
  console.log(`[ATHENA] ONNX model loaded successfully (${loadTime.toFixed(0)}ms)`);
@@ -108,7 +125,7 @@ class OnDeviceInference {
108
125
  const startTime = performance.now();
109
126
  try {
110
127
  // Create input tensor
111
- const inputTensor = new Tensor('float32', features, [1, features.length]);
128
+ const inputTensor = new Tensor$1('float32', features, [1, features.length]);
112
129
  const feeds = {
113
130
  input: inputTensor
114
131
  };
@@ -234,6 +251,192 @@ class OnDeviceInference {
234
251
  }
235
252
  }
236
253
 
254
+ /**
255
+ * On-Device Inference Module (Web Browser)
256
+ *
257
+ * Uses onnxruntime-web for browser-based ML inference
258
+ * Target latency: <3ms P95
259
+ */
260
+ let InferenceSession;
261
+ let Tensor;
262
+ // Dynamically import ONNX Runtime Web (only available in browsers)
263
+ async function loadOnnxRuntimeWeb() {
264
+ try {
265
+ const onnx = await import('onnxruntime-web');
266
+ InferenceSession = onnx.InferenceSession;
267
+ Tensor = onnx.Tensor;
268
+ return true;
269
+ } catch (error) {
270
+ console.error('[ATHENA] Failed to load onnxruntime-web:', error);
271
+ return false;
272
+ }
273
+ }
274
+ class OnDeviceInferenceWeb {
275
+ constructor() {
276
+ this.session = null;
277
+ this.modelLoaded = false;
278
+ }
279
+ /**
280
+ * Initialize ONNX session with model file
281
+ */
282
+ async initialize(modelPath) {
283
+ console.log('[ATHENA] Loading ONNX model for browser inference...');
284
+ // Load ONNX Runtime Web
285
+ const loaded = await loadOnnxRuntimeWeb();
286
+ if (!loaded) {
287
+ throw new Error('onnxruntime-web not available');
288
+ }
289
+ try {
290
+ const startTime = performance.now();
291
+ this.session = await InferenceSession.create(modelPath, {
292
+ executionProviders: ['wasm'],
293
+ // Use WebAssembly backend
294
+ graphOptimizationLevel: 'all'
295
+ });
296
+ const loadTime = performance.now() - startTime;
297
+ this.modelLoaded = true;
298
+ console.log(`[ATHENA] ONNX model loaded successfully in browser (${loadTime.toFixed(0)}ms)`);
299
+ } catch (error) {
300
+ console.error('[ATHENA] Failed to load ONNX model:', error);
301
+ throw new Error(`Model loading failed: ${error}`);
302
+ }
303
+ }
304
+ /**
305
+ * Run inference on feature vector
306
+ */
307
+ async predict(features) {
308
+ if (!this.modelLoaded || !this.session) {
309
+ throw new Error('ONNX model not initialized. Call initialize() first.');
310
+ }
311
+ const startTime = performance.now();
312
+ try {
313
+ // Create input tensor
314
+ const inputTensor = new Tensor('float32', features, [1, features.length]);
315
+ const feeds = {
316
+ input: inputTensor
317
+ };
318
+ // Run inference
319
+ const results = await this.session.run(feeds);
320
+ const inferenceTime = performance.now() - startTime;
321
+ // Parse output
322
+ const prediction = this.parseOutput(results.output);
323
+ console.log(`[ATHENA] Browser inference complete: ${inferenceTime.toFixed(2)}ms ` + `(class: ${prediction.predicted_class}, confidence: ${prediction.confidence.toFixed(2)})`);
324
+ return {
325
+ ...prediction,
326
+ inference_time_ms: Math.round(inferenceTime * 100) / 100,
327
+ inference_location: 'on-device',
328
+ timestamp: new Date().toISOString()
329
+ };
330
+ } catch (error) {
331
+ console.error('[ATHENA] Browser inference failed:', error);
332
+ throw new Error(`Inference failed: ${error}`);
333
+ }
334
+ }
335
+ /**
336
+ * Parse ONNX output tensor to prediction result
337
+ */
338
+ parseOutput(outputTensor) {
339
+ const data = outputTensor.data;
340
+ // Extract predictions from model output
341
+ // Model outputs: [predicted_class_idx, confidence, purchase_intent, cart_abandon_risk, checkout_abandon_risk]
342
+ const predictedClassIdx = data[0];
343
+ const confidence = data[1];
344
+ const purchaseIntent = data[2];
345
+ const cartAbandonRisk = data[3] || 0;
346
+ const checkoutAbandonRisk = data[4] || 0;
347
+ // Map class index to label
348
+ const classLabels = ['engaged_explorer', 'high_intent_buyer', 'cart_abandoner', 'checkout_abandoner', 'bounce_risk'];
349
+ const predictedClass = classLabels[predictedClassIdx] || 'unknown';
350
+ // Determine archetype based on purchase intent
351
+ let archetype;
352
+ if (purchaseIntent >= 0.85) {
353
+ archetype = 'fast_mover';
354
+ } else if (purchaseIntent >= 0.60) {
355
+ archetype = 'on_track';
356
+ } else if (purchaseIntent >= 0.40) {
357
+ archetype = 'slow_adopter';
358
+ } else if (purchaseIntent >= 0.20) {
359
+ archetype = 'at_risk';
360
+ } else {
361
+ archetype = 'different_path';
362
+ }
363
+ // Generate recommendations
364
+ const recommendation = this.generateRecommendation(predictedClass, purchaseIntent, cartAbandonRisk, checkoutAbandonRisk);
365
+ return {
366
+ predicted_class: predictedClass,
367
+ confidence,
368
+ archetype,
369
+ purchase_intent: purchaseIntent,
370
+ cart_abandonment_risk: cartAbandonRisk,
371
+ checkout_abandonment_risk: checkoutAbandonRisk,
372
+ ...recommendation
373
+ };
374
+ }
375
+ /**
376
+ * Generate action recommendations based on prediction
377
+ */
378
+ generateRecommendation(predictedClass, purchaseIntent, cartAbandonRisk, checkoutAbandonRisk) {
379
+ // High cart abandonment risk
380
+ if (cartAbandonRisk > 0.7) {
381
+ return {
382
+ recommended_action: 'Show cart abandonment discount (10-15% off)',
383
+ urgency: 'high',
384
+ trigger_reason: `High cart abandonment risk (${(cartAbandonRisk * 100).toFixed(0)}%)`
385
+ };
386
+ }
387
+ // High checkout abandonment risk
388
+ if (checkoutAbandonRisk > 0.7) {
389
+ return {
390
+ recommended_action: 'Simplify checkout flow or offer free shipping',
391
+ urgency: 'critical',
392
+ trigger_reason: `High checkout abandonment risk (${(checkoutAbandonRisk * 100).toFixed(0)}%)`
393
+ };
394
+ }
395
+ // High purchase intent
396
+ if (purchaseIntent > 0.8) {
397
+ return {
398
+ recommended_action: 'Show product recommendations or upsell',
399
+ urgency: 'medium',
400
+ trigger_reason: `High purchase intent (${(purchaseIntent * 100).toFixed(0)}%)`
401
+ };
402
+ }
403
+ // Low purchase intent
404
+ if (purchaseIntent < 0.3) {
405
+ return {
406
+ recommended_action: 'Show value proposition or testimonials',
407
+ urgency: 'low',
408
+ trigger_reason: `Low purchase intent (${(purchaseIntent * 100).toFixed(0)}%)`
409
+ };
410
+ }
411
+ return {
412
+ recommended_action: 'Continue monitoring',
413
+ urgency: 'low'
414
+ };
415
+ }
416
+ /**
417
+ * Check if model is loaded
418
+ */
419
+ isReady() {
420
+ return this.modelLoaded;
421
+ }
422
+ /**
423
+ * Cleanup resources
424
+ */
425
+ async dispose() {
426
+ if (this.session) {
427
+ try {
428
+ // ONNX Runtime sessions should be disposed
429
+ await this.session.release?.();
430
+ console.log('[ATHENA] Browser inference session disposed');
431
+ } catch (error) {
432
+ console.warn('[ATHENA] Failed to dispose session:', error);
433
+ }
434
+ this.session = null;
435
+ this.modelLoaded = false;
436
+ }
437
+ }
438
+ }
439
+
237
440
  /**
238
441
  * Server-Side Inference Module
239
442
  *
@@ -314,12 +517,17 @@ class ServerInference {
314
517
  try {
315
518
  // Replace /v1/predict with /health for health check
316
519
  const healthUrl = this.apiUrl.replace('/v1/predict', '/health');
520
+ console.log(`[ATHENA] Testing server connectivity: ${healthUrl}`);
317
521
  const response = await this.fetchWithTimeout(healthUrl, {
318
522
  method: 'GET'
319
- }, 10000); // Increased to 10s for mobile networks
523
+ }, Math.min(this.timeout, 5000)); // Use 5s max for health check
524
+ if (response.ok) {
525
+ console.log('[ATHENA] Server connectivity test passed');
526
+ }
320
527
  return response.ok;
321
528
  } catch (error) {
322
- console.warn('[ATHENA] Server connectivity test failed:', error);
529
+ console.warn('[ATHENA] Server connectivity test failed:', error.message || error);
530
+ console.warn('[ATHENA] URL attempted:', this.apiUrl.replace('/v1/predict', '/health'));
323
531
  return false;
324
532
  }
325
533
  }
@@ -356,6 +564,10 @@ class AthenaTracker {
356
564
  this.onDeviceInference = null;
357
565
  this.serverInference = null;
358
566
  this.batchIntervalId = null;
567
+ this.failureCount = 0;
568
+ this.maxFailures = 3;
569
+ // External IDs for custom audience building (Meta, Google)
570
+ this.externalIds = {};
359
571
  // Private constructor for singleton
360
572
  }
361
573
  /**
@@ -398,6 +610,10 @@ class AthenaTracker {
398
610
  }
399
611
  // Generate session ID
400
612
  instance.state.sessionId = instance.generateSessionId();
613
+ // Generate anonymous user ID (will be replaced on identify())
614
+ instance.state.userId = instance.generateAnonymousId();
615
+ // Capture external IDs for custom audience building (browser only)
616
+ instance.captureExternalIds();
401
617
  // Start event batching
402
618
  instance.startBatching();
403
619
  instance.state.initialized = true;
@@ -408,7 +624,15 @@ class AthenaTracker {
408
624
  */
409
625
  async initializeOnDevice() {
410
626
  try {
411
- this.onDeviceInference = new OnDeviceInference();
627
+ const platform = getPlatform();
628
+ // Use platform-specific ONNX runtime
629
+ if (platform === 'web') {
630
+ this.onDeviceInference = new OnDeviceInferenceWeb();
631
+ } else if (platform === 'react-native') {
632
+ this.onDeviceInference = new OnDeviceInference();
633
+ } else {
634
+ throw new Error('Unsupported platform for on-device inference');
635
+ }
412
636
  await this.onDeviceInference.initialize(this.config.modelPath);
413
637
  console.log('[ATHENA] On-device inference ready');
414
638
  } catch (error) {
@@ -422,7 +646,8 @@ class AthenaTracker {
422
646
  * Initialize server-side inference
423
647
  */
424
648
  async initializeServer() {
425
- this.serverInference = new ServerInference(this.config.serverInferenceUrl, this.config.appToken);
649
+ this.serverInference = new ServerInference(this.config.serverInferenceUrl, this.config.appToken, this.config.timeout || 5000 // Use configured timeout or default to 5s
650
+ );
426
651
  // Test connectivity
427
652
  const connected = await this.serverInference.testConnection();
428
653
  if (!connected) {
@@ -433,15 +658,37 @@ class AthenaTracker {
433
658
  }
434
659
  /**
435
660
  * Identify a user
661
+ * Supports email/phone hashing for Meta/Google custom audiences
662
+ *
663
+ * @param userId - User identifier
664
+ * @param traits - User traits (email, phone, name, etc.)
665
+ * @param options - Optional configuration
436
666
  */
437
- static identify(userId, traits) {
667
+ static async identify(userId, traits, options) {
438
668
  const instance = AthenaTracker.getInstance();
669
+ const previousUserId = instance.state.userId; // Store anonymous ID for mapping
439
670
  instance.state.userId = userId;
671
+ // Hash PII for custom audience building if requested
672
+ const hashedTraits = {
673
+ ...traits
674
+ };
675
+ if (options?.hashPII && traits) {
676
+ if (traits.email) {
677
+ hashedTraits.email_sha256 = await instance.hashPII(traits.email);
678
+ }
679
+ if (traits.phone) {
680
+ hashedTraits.phone_sha256 = await instance.hashPII(traits.phone);
681
+ }
682
+ }
440
683
  AthenaTracker.track('identify', {
441
684
  user_id: userId,
442
- ...traits
685
+ anonymous_id: previousUserId,
686
+ // Map anonymous → identified user
687
+ ...hashedTraits,
688
+ // Include external IDs for audience sync
689
+ external_ids: Object.keys(instance.externalIds).length > 0 ? instance.externalIds : undefined
443
690
  });
444
- console.log(`[ATHENA] User identified: ${userId}`);
691
+ console.log(`[ATHENA] User identified: ${userId}` + (previousUserId?.startsWith('anon_') ? ` (was ${previousUserId})` : ''));
445
692
  }
446
693
  /**
447
694
  * Track an event
@@ -473,10 +720,21 @@ class AthenaTracker {
473
720
  */
474
721
  async processBatch() {
475
722
  if (this.state.events.length === 0) return;
723
+ // Skip inference if too many consecutive failures
724
+ if (this.failureCount >= this.maxFailures) {
725
+ if (this.config?.debug) {
726
+ console.warn(`[ATHENA] Skipping inference after ${this.failureCount} consecutive failures. Will retry after ${this.maxFailures} successful events.`);
727
+ }
728
+ // Reset failure count after clearing queue
729
+ this.failureCount = Math.max(0, this.failureCount - 1);
730
+ this.state.events = []; // Drop events to prevent infinite buildup
731
+ return;
732
+ }
476
733
  const events = [...this.state.events];
477
734
  this.state.events = [];
478
735
  try {
479
736
  const prediction = await this.runInference(events);
737
+ this.failureCount = 0; // Reset on success
480
738
  if (this.config?.webhook?.enabled && this.config.webhook.url) {
481
739
  await this.sendWebhook(prediction);
482
740
  }
@@ -484,9 +742,14 @@ class AthenaTracker {
484
742
  console.log('[ATHENA] Prediction:', prediction);
485
743
  }
486
744
  } catch (error) {
487
- console.error('[ATHENA] Failed to process batch:', error);
488
- // Re-queue events on failure
489
- this.state.events.unshift(...events);
745
+ this.failureCount++;
746
+ console.error(`[ATHENA] Failed to process batch (${this.failureCount}/${this.maxFailures}):`, error.message || error);
747
+ // Only re-queue if below failure threshold
748
+ if (this.failureCount < this.maxFailures) {
749
+ this.state.events.unshift(...events);
750
+ } else {
751
+ console.warn('[ATHENA] Max failures reached. Dropping events and disabling inference temporarily.');
752
+ }
490
753
  }
491
754
  }
492
755
  /**
@@ -549,7 +812,9 @@ class AthenaTracker {
549
812
  body: JSON.stringify({
550
813
  ...prediction,
551
814
  session_id: this.state.sessionId,
552
- user_id: this.state.userId
815
+ user_id: this.state.userId,
816
+ // Include external IDs for custom audience building
817
+ external_ids: Object.keys(this.externalIds).length > 0 ? this.externalIds : undefined
553
818
  })
554
819
  });
555
820
  if (response.ok) {
@@ -618,6 +883,82 @@ class AthenaTracker {
618
883
  generateSessionId() {
619
884
  return `sess_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
620
885
  }
886
+ /**
887
+ * Generate anonymous user ID compatible with Meta/Google standards
888
+ * Format: anon_{timestamp}_{random} for Pascal internal use
889
+ * Can be mapped to hashed PII for custom audience building
890
+ */
891
+ generateAnonymousId() {
892
+ const timestamp = Date.now();
893
+ const random = Math.random().toString(36).substring(2, 11);
894
+ return `anon_${timestamp}_${random}`;
895
+ }
896
+ /**
897
+ * Capture external IDs from Meta and Google for custom audience building
898
+ * Only works in browser environment with cookies
899
+ */
900
+ captureExternalIds() {
901
+ // Skip if not in browser environment
902
+ if (typeof document === 'undefined' || typeof document.cookie === 'undefined') {
903
+ return;
904
+ }
905
+ try {
906
+ const cookies = document.cookie.split(';').reduce((acc, cookie) => {
907
+ const [key, value] = cookie.trim().split('=');
908
+ acc[key] = value;
909
+ return acc;
910
+ }, {});
911
+ // Meta Pixel ID (_fbp) - Format: fb.{version}.{timestamp}.{random}
912
+ if (cookies._fbp) {
913
+ this.externalIds.fbp = cookies._fbp;
914
+ }
915
+ // Meta Click ID (_fbc) - Format: fb.{version}.{timestamp}.{click_id}
916
+ if (cookies._fbc) {
917
+ this.externalIds.fbc = cookies._fbc;
918
+ }
919
+ // Google Analytics Client ID (_ga) - Format: GA1.{version}.{random}.{timestamp}
920
+ if (cookies._ga) {
921
+ this.externalIds.ga = cookies._ga;
922
+ }
923
+ // Google Click ID (gcl_aw) - For Google Ads conversions
924
+ if (cookies.gcl_aw) {
925
+ this.externalIds.gclid = cookies.gcl_aw;
926
+ }
927
+ if (this.config?.debug && Object.keys(this.externalIds).length > 0) {
928
+ console.log('[ATHENA] External IDs captured for audience building:', this.externalIds);
929
+ }
930
+ } catch (error) {
931
+ console.warn('[ATHENA] Failed to capture external IDs:', error);
932
+ }
933
+ }
934
+ /**
935
+ * Hash PII for custom audience matching (SHA-256)
936
+ * Use this when user identifies with email/phone
937
+ */
938
+ async hashPII(value) {
939
+ // Normalize: lowercase, trim whitespace
940
+ const normalized = value.toLowerCase().trim();
941
+ // Use Web Crypto API for hashing (browser/modern Node)
942
+ if (typeof crypto !== 'undefined' && crypto.subtle) {
943
+ const encoder = new TextEncoder();
944
+ const data = encoder.encode(normalized);
945
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
946
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
947
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
948
+ }
949
+ // Try expo-crypto for React Native (Expo)
950
+ try {
951
+ // @ts-expect-error - expo-crypto is an optional peer dependency
952
+ const Crypto = await import('expo-crypto');
953
+ const hash = await Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, normalized);
954
+ return hash;
955
+ } catch (error) {
956
+ // expo-crypto not available
957
+ }
958
+ // Fallback: return unhashed (server should hash it)
959
+ console.warn('[ATHENA] SHA-256 not available - install expo-crypto for React Native: npx expo install expo-crypto');
960
+ return normalized;
961
+ }
621
962
  /**
622
963
  * Sleep utility
623
964
  */
@@ -634,13 +975,13 @@ AthenaTracker.instance = null;
634
975
  * Per ATHENA Integration Plan Phase 4.1
635
976
  */
636
977
  // Cross-platform crypto support
637
- let crypto;
978
+ let crypto$1;
638
979
  try {
639
980
  // Node.js environment
640
- crypto = require('crypto');
981
+ crypto$1 = require('crypto');
641
982
  } catch {
642
983
  // Browser/React Native - will use Web Crypto API
643
- crypto = null;
984
+ crypto$1 = null;
644
985
  }
645
986
  /**
646
987
  * Generate HMAC-SHA256 signature for webhook payload (Node.js version)
@@ -651,7 +992,7 @@ try {
651
992
  */
652
993
  function generateWebhookSignatureNode(payload, secret) {
653
994
  const payloadString = JSON.stringify(payload);
654
- const hmac = crypto.createHmac('sha256', secret);
995
+ const hmac = crypto$1.createHmac('sha256', secret);
655
996
  hmac.update(payloadString);
656
997
  return hmac.digest('hex');
657
998
  }
@@ -685,7 +1026,7 @@ async function generateWebhookSignatureBrowser(payload, secret) {
685
1026
  * @returns HMAC signature (hex-encoded)
686
1027
  */
687
1028
  async function generateWebhookSignature(payload, secret) {
688
- if (crypto) {
1029
+ if (crypto$1) {
689
1030
  // Node.js environment
690
1031
  return generateWebhookSignatureNode(payload, secret);
691
1032
  } else if (typeof globalThis !== 'undefined' && globalThis.crypto?.subtle) {
@@ -712,9 +1053,9 @@ async function verifyWebhookSignature(payload, signature, secret) {
712
1053
  return true;
713
1054
  }
714
1055
  // Use timing-safe comparison to prevent timing attacks (Node.js only)
715
- if (crypto && crypto.timingSafeEqual) {
1056
+ if (crypto$1 && crypto$1.timingSafeEqual) {
716
1057
  try {
717
- return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expectedSignature, 'hex'));
1058
+ return crypto$1.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expectedSignature, 'hex'));
718
1059
  } catch (error) {
719
1060
  return false;
720
1061
  }