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