@keverdjs/fraud-sdk-react 1.1.2 → 3.0.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.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { createContext, useRef, useState, useEffect, useContext, useCallback } from 'react';
1
+ import { Keverd } from '@keverdjs/fraud-sdk';
2
+ import { createContext, useRef, useState, useEffect, useContext, useCallback, useMemo } from 'react';
2
3
  import { jsx } from 'react/jsx-runtime';
3
4
 
4
5
  // src/collectors/keverd-device-collector.ts
@@ -631,8 +632,6 @@ var KeverdBehavioralCollector = class {
631
632
  }, 0);
632
633
  }
633
634
  };
634
-
635
- // src/core/keverd-sdk.ts
636
635
  var KeverdSDK = class {
637
636
  constructor() {
638
637
  this.config = null;
@@ -651,9 +650,6 @@ var KeverdSDK = class {
651
650
  }
652
651
  return;
653
652
  }
654
- if (!config.apiKey) {
655
- throw new Error("Keverd SDK: apiKey is required");
656
- }
657
653
  this.config = {
658
654
  debug: false,
659
655
  ...config
@@ -664,8 +660,6 @@ var KeverdSDK = class {
664
660
  if (this.config.debug) {
665
661
  console.log("[Keverd SDK] Initialized successfully");
666
662
  }
667
- this.startSession().catch(() => {
668
- });
669
663
  }
670
664
  /**
671
665
  * Get visitor data (fingerprint and risk assessment)
@@ -687,7 +681,8 @@ var KeverdSDK = class {
687
681
  session: sessionInfo,
688
682
  behavioral: behavioralData
689
683
  };
690
- const response = await this.sendFingerprintRequest(request, options);
684
+ const privacySignals = this.collectPrivacySignals();
685
+ const response = await this.sendFingerprintRequest(request, options, privacySignals);
691
686
  return this.transformResponse(response);
692
687
  } catch (error) {
693
688
  const keverdError = {
@@ -700,6 +695,27 @@ var KeverdSDK = class {
700
695
  throw keverdError;
701
696
  }
702
697
  }
698
+ /**
699
+ * Verify user identity during login attempts.
700
+ *
701
+ * This is a thin wrapper around the core web SDK `verifyLogin`, so it sends
702
+ * the same enhanced signals and structured login context as the vanilla SDK.
703
+ */
704
+ async verifyLogin(options) {
705
+ if (!this.isInitialized || !this.config) {
706
+ throw new Error("Keverd SDK not initialized. Call init() first.");
707
+ }
708
+ if (!this.config.apiKey) {
709
+ throw new Error("Keverd SDK: apiKey is required for verifyLogin");
710
+ }
711
+ Keverd.init({
712
+ apiKey: this.config.apiKey,
713
+ userId: this.config.userId,
714
+ endpoint: this.getDefaultEndpoint(),
715
+ debug: this.config.debug || false
716
+ });
717
+ return Keverd.verifyLogin(options);
718
+ }
703
719
  /**
704
720
  * Extract origin and referrer from browser
705
721
  */
@@ -714,11 +730,11 @@ var KeverdSDK = class {
714
730
  /**
715
731
  * Send fingerprint request to backend
716
732
  */
717
- async sendFingerprintRequest(request, options) {
733
+ async sendFingerprintRequest(request, options, privacySignals) {
718
734
  if (!this.config) {
719
735
  throw new Error("SDK not initialized");
720
736
  }
721
- const url = `${this.getDefaultEndpoint()}/fingerprint/score`;
737
+ const url = `${this.getDefaultEndpoint()}/v2/fingerprint`;
722
738
  const headers = {
723
739
  "Content-Type": "application/json",
724
740
  "X-SDK-Source": "react"
@@ -737,20 +753,11 @@ var KeverdSDK = class {
737
753
  headers["X-API-KEY"] = apiKey;
738
754
  headers["Authorization"] = `Bearer ${apiKey}`;
739
755
  }
740
- if (options?.tag === "sandbox") {
741
- headers["X-Sandbox"] = "true";
742
- }
743
- const requestBody = {
744
- ...request,
745
- session: {
746
- ...request.session || {},
747
- sessionId: this.sessionId || request.session?.sessionId
748
- }
749
- };
756
+ const signalsPayload = this.buildV2SignalsPayload(request, privacySignals);
750
757
  const response = await fetch(url, {
751
758
  method: "POST",
752
759
  headers,
753
- body: JSON.stringify(requestBody)
760
+ body: JSON.stringify({ signals: signalsPayload })
754
761
  });
755
762
  if (!response.ok) {
756
763
  const errorText = await response.text().catch(() => "Unknown error");
@@ -771,25 +778,345 @@ var KeverdSDK = class {
771
778
  * Transform API response to visitor data format
772
779
  */
773
780
  transformResponse(response) {
781
+ const fingerprintId = response.device_id || response.fingerprint;
782
+ if (fingerprintId) {
783
+ const similarity = typeof response.similarity === "number" ? response.similarity : 0;
784
+ const score = Number((1 - similarity).toFixed(3));
785
+ const riskScore = Math.round(score * 100);
786
+ const reasons = [];
787
+ if (response.is_new) reasons.push("new_device");
788
+ if (response.is_drifted) reasons.push("device_drifted");
789
+ if (reasons.length === 0) reasons.push("known_device_match");
790
+ const requestId = response.requestId || response.event_id || fingerprintId;
791
+ const historyCount = response.history_count ?? response.times_seen ?? 1;
792
+ return {
793
+ visitorId: fingerprintId,
794
+ riskScore,
795
+ score,
796
+ action: this.actionFromRisk(riskScore),
797
+ reasons,
798
+ sessionId: this.sessionId || fingerprintId,
799
+ requestId,
800
+ confidence: similarity,
801
+ extendedResult: {
802
+ fingerprint: fingerprintId,
803
+ eventId: requestId,
804
+ similarity,
805
+ isNew: response.is_new ?? false,
806
+ isDrifted: response.is_drifted ?? false,
807
+ visitCount: historyCount,
808
+ historyCount,
809
+ firstSeen: response.first_seen,
810
+ lastSeen: response.last_seen || (/* @__PURE__ */ new Date()).toISOString(),
811
+ browser: {
812
+ name: this._detectBrowser(),
813
+ version: void 0
814
+ },
815
+ network: {
816
+ anonymizer: "Unknown"
817
+ }
818
+ }
819
+ };
820
+ }
774
821
  return {
775
- visitorId: response.requestId,
776
- riskScore: response.risk_score,
777
- score: response.score,
778
- action: response.action,
822
+ visitorId: response.requestId || this.sessionId || this.generateSessionId(),
823
+ riskScore: response.risk_score ?? 0,
824
+ score: response.score ?? 0,
825
+ action: response.action ?? "allow",
779
826
  reasons: response.reason || [],
780
- sessionId: response.session_id,
781
- requestId: response.requestId,
827
+ sessionId: response.session_id || this.sessionId || this.generateSessionId(),
828
+ requestId: response.requestId || this.sessionId || this.generateSessionId(),
782
829
  simSwapEngine: response.sim_swap_engine,
783
- confidence: 1 - response.score
784
- // Inverse of risk score as confidence
830
+ confidence: response.confidence ?? 1 - (response.score ?? 0),
831
+ // Prefer backend confidence when present
832
+ deviceMatch: response.device_match,
833
+ fraudProbability: response.fraud_probability,
834
+ recommendedAction: response.recommended_action,
835
+ adaptiveResponse: response.adaptive_response ? {
836
+ recommendedAction: response.adaptive_response.recommended_action,
837
+ challenges: Array.isArray(response.adaptive_response.challenges) ? response.adaptive_response.challenges : void 0,
838
+ reason: response.adaptive_response.reason,
839
+ confidence: response.adaptive_response.confidence
840
+ } : void 0
841
+ };
842
+ }
843
+ buildV2SignalsPayload(request, privacySignals) {
844
+ const behavioral = request.behavioral || {};
845
+ const device = request.device;
846
+ const session = request.session || {};
847
+ const webgl = this.getWebGLInfo();
848
+ const browserInfo = this.parseBrowserInfo();
849
+ const osInfo = this.parseOsInfo();
850
+ const audioHash = this.computeSimpleHash(
851
+ `${navigator.userAgent}|${navigator.language}|${device.fingerprint}`
852
+ );
853
+ const detectedFonts = this.detectBasicFonts();
854
+ const screenWidth = Number(device.screenWidth ?? window.screen.width);
855
+ const screenHeight = Number(device.screenHeight ?? window.screen.height);
856
+ const maxTouchPoints = Number(navigator.maxTouchPoints || 0);
857
+ const touchSupport = maxTouchPoints > 0 || "ontouchstart" in window;
858
+ const memory = Number(navigator.deviceMemory || 0);
859
+ const concurrency = Number(navigator.hardwareConcurrency || 0);
860
+ const plugins = Array.from(navigator.plugins || []).map((plugin) => plugin.name).filter(Boolean).slice(0, 20);
861
+ const storageFlags = this.getStorageFlags();
862
+ const isIncognitoByStorage = !storageFlags.local_storage && !storageFlags.session_storage && !storageFlags.cookies;
863
+ const isIncognito = (privacySignals?.isIncognito ?? false) || isIncognitoByStorage;
864
+ const payload = {
865
+ hardware: {
866
+ ...memory > 0 ? { memory, device_memory: memory } : {},
867
+ concurrency,
868
+ screen_width: Number.isFinite(screenWidth) ? screenWidth : window.screen.width,
869
+ screen_height: Number.isFinite(screenHeight) ? screenHeight : window.screen.height,
870
+ color_depth: window.screen.colorDepth,
871
+ device_pixel_ratio: window.devicePixelRatio || 1,
872
+ touch_support: touchSupport,
873
+ max_touch_points: maxTouchPoints,
874
+ platform: navigator.platform || "unknown",
875
+ architecture: navigator.platform || "unknown",
876
+ device: device.device,
877
+ timezone: device.timezone,
878
+ languages: Array.isArray(navigator.languages) ? navigator.languages : [navigator.language],
879
+ gpu: {
880
+ renderer: webgl.renderer !== "unknown" ? webgl.renderer : "extraction-failed",
881
+ vendor: webgl.vendor !== "unknown" ? webgl.vendor : "extraction-failed",
882
+ version: webgl.version || void 0,
883
+ shading_language_version: webgl.shadingLanguageVersion || void 0
884
+ }
885
+ },
886
+ graphics: {
887
+ vendor: webgl.vendor,
888
+ renderer: webgl.renderer,
889
+ canvas_hash: device.fingerprint,
890
+ canvas: {
891
+ hash_webgl: this.computeSimpleHash(`${webgl.vendor}|${webgl.renderer}|${webgl.version || ""}`),
892
+ hash_winding: this.computeSimpleHash(`${device.fingerprint}|winding`),
893
+ hash_2d: this.computeSimpleHash(`${device.fingerprint}|2d`),
894
+ hash_text: this.computeSimpleHash(`${device.fingerprint}|text`)
895
+ },
896
+ webgl: {
897
+ vendor: webgl.vendor,
898
+ renderer: webgl.renderer,
899
+ version: webgl.version || void 0,
900
+ shading_language_version: webgl.shadingLanguageVersion || void 0
901
+ }
902
+ },
903
+ audio: {
904
+ fingerprint: audioHash,
905
+ audio_hash: audioHash,
906
+ available: true,
907
+ sample_rate: 44100,
908
+ codecs: this.getAudioCodecs()
909
+ },
910
+ behavioral: {
911
+ mouse_speed: behavioral.swipe_velocity ?? 0,
912
+ languages: Array.isArray(navigator.languages) ? navigator.languages : [navigator.language],
913
+ timezone: device.timezone
914
+ },
915
+ software: {
916
+ os: osInfo.os,
917
+ browser: browserInfo.browser,
918
+ version: browserInfo.version,
919
+ platform: navigator.platform,
920
+ ua: navigator.userAgent,
921
+ fonts: detectedFonts.slice(0, 20),
922
+ plugins,
923
+ storage_flags: storageFlags,
924
+ do_not_track: navigator.doNotTrack ?? null
925
+ },
926
+ network: {
927
+ ip_isp: "unknown",
928
+ vpn: privacySignals?.isVPN ?? false,
929
+ vpn_detected: privacySignals?.isVPN ?? false,
930
+ proxy: false,
931
+ tor: false
932
+ },
933
+ session: {
934
+ incognito: isIncognito,
935
+ session_id: this.sessionId || session.sessionId || request.userId || this.generateSessionId(),
936
+ bot: privacySignals?.isAutomated ?? false,
937
+ automation: privacySignals?.isAutomated ?? false,
938
+ hasAdBlocker: privacySignals?.hasAdBlocker ?? false,
939
+ has_ad_blocker: privacySignals?.hasAdBlocker ?? false
940
+ },
941
+ confidence: 0.9
942
+ };
943
+ const persistenceScore = this.calculatePersistenceScore(payload);
944
+ const fallback = persistenceScore < 0.7;
945
+ payload._metadata = {
946
+ fallback,
947
+ fallback_reason: fallback ? "incognito_restrictions" : void 0,
948
+ sdk_version: "2.0.0",
949
+ persistence_score: persistenceScore,
950
+ collected_at: Date.now()
951
+ };
952
+ if (fallback) {
953
+ return {
954
+ hardware: payload.hardware,
955
+ graphics: {
956
+ canvas: {
957
+ hash_webgl: payload.graphics?.canvas?.hash_webgl,
958
+ hash_winding: payload.graphics?.canvas?.hash_winding
959
+ },
960
+ webgl: {
961
+ vendor: payload.graphics?.webgl?.vendor,
962
+ renderer: payload.graphics?.webgl?.renderer
963
+ }
964
+ },
965
+ session: payload.session,
966
+ _metadata: payload._metadata
967
+ };
968
+ }
969
+ return payload;
970
+ }
971
+ getWebGLInfo() {
972
+ try {
973
+ const canvas = document.createElement("canvas");
974
+ const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
975
+ if (!gl) {
976
+ return { vendor: "unknown", renderer: "unknown" };
977
+ }
978
+ const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
979
+ if (debugInfo) {
980
+ return {
981
+ vendor: String(gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || "unknown"),
982
+ renderer: String(gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || "unknown"),
983
+ version: String(gl.getParameter(gl.VERSION) || "unknown"),
984
+ shadingLanguageVersion: String(gl.getParameter(gl.SHADING_LANGUAGE_VERSION) || "unknown")
985
+ };
986
+ }
987
+ return {
988
+ vendor: String(gl.getParameter(gl.VENDOR) || "unknown"),
989
+ renderer: String(gl.getParameter(gl.RENDERER) || "unknown"),
990
+ version: String(gl.getParameter(gl.VERSION) || "unknown"),
991
+ shadingLanguageVersion: String(gl.getParameter(gl.SHADING_LANGUAGE_VERSION) || "unknown")
992
+ };
993
+ } catch {
994
+ return { vendor: "unknown", renderer: "unknown" };
995
+ }
996
+ }
997
+ parseBrowserInfo() {
998
+ const ua = navigator.userAgent;
999
+ const browser = this._detectBrowser();
1000
+ const versionMatch = ua.match(/Chrome\/([\d.]+)/) || ua.match(/Firefox\/([\d.]+)/) || ua.match(/Version\/([\d.]+)/) || ua.match(/Edg\/([\d.]+)/);
1001
+ return { browser, version: versionMatch?.[1] || "unknown" };
1002
+ }
1003
+ parseOsInfo() {
1004
+ const ua = navigator.userAgent;
1005
+ if (ua.includes("Mac OS X")) return { os: "Mac OS X" };
1006
+ if (ua.includes("Windows")) return { os: "Windows" };
1007
+ if (ua.includes("Android")) return { os: "Android" };
1008
+ if (ua.includes("iPhone") || ua.includes("iPad")) return { os: "iOS" };
1009
+ if (ua.includes("Linux")) return { os: "Linux" };
1010
+ return { os: "unknown" };
1011
+ }
1012
+ detectBasicFonts() {
1013
+ const commonFonts = ["Arial", "Helvetica", "Verdana", "Times New Roman", "Courier New"];
1014
+ return commonFonts.slice(0, 3);
1015
+ }
1016
+ calculatePersistenceScore(payload) {
1017
+ let score = 0;
1018
+ if (payload.hardware?.gpu?.renderer && payload.hardware.gpu.renderer !== "extraction-failed") score += 0.25;
1019
+ if (payload.hardware?.screen_width && payload.hardware?.screen_height) score += 0.15;
1020
+ if ((payload.hardware?.concurrency || 0) > 0) score += 0.1;
1021
+ if (payload.graphics?.webgl?.renderer || payload.graphics?.webgl?.vendor) score += 0.2;
1022
+ if (payload.graphics?.canvas?.hash_webgl) score += 0.15;
1023
+ if (payload.audio?.fingerprint) score += 0.1;
1024
+ if ((payload.software?.fonts || []).length > 0) score += 0.03;
1025
+ if ((payload.software?.plugins || []).length > 0) score += 0.02;
1026
+ return Number(Math.min(1, score).toFixed(2));
1027
+ }
1028
+ getStorageFlags() {
1029
+ return {
1030
+ local_storage: this.isApiAvailable(() => window.localStorage),
1031
+ session_storage: this.isApiAvailable(() => window.sessionStorage),
1032
+ indexed_db: this.isApiAvailable(() => window.indexedDB),
1033
+ cookies: navigator.cookieEnabled === true
785
1034
  };
786
1035
  }
1036
+ isApiAvailable(accessor) {
1037
+ try {
1038
+ return typeof accessor() !== "undefined";
1039
+ } catch {
1040
+ return false;
1041
+ }
1042
+ }
1043
+ getAudioCodecs() {
1044
+ const audio = document.createElement("audio");
1045
+ return {
1046
+ mp3: audio.canPlayType("audio/mpeg"),
1047
+ ogg: audio.canPlayType("audio/ogg"),
1048
+ aac: audio.canPlayType("audio/aac"),
1049
+ opus: audio.canPlayType("audio/ogg; codecs=opus")
1050
+ };
1051
+ }
1052
+ collectPrivacySignals() {
1053
+ const isIncognito = this.isStorageUnavailable();
1054
+ return {
1055
+ isIncognito,
1056
+ isVPN: false,
1057
+ isAutomated: navigator.webdriver === true,
1058
+ hasAdBlocker: this.detectAdBlocker()
1059
+ };
1060
+ }
1061
+ isStorageUnavailable() {
1062
+ try {
1063
+ const hasLocal = typeof window.localStorage !== "undefined";
1064
+ const hasSession = typeof window.sessionStorage !== "undefined";
1065
+ const cookies = navigator.cookieEnabled === true;
1066
+ return !hasLocal && !hasSession && !cookies;
1067
+ } catch {
1068
+ return true;
1069
+ }
1070
+ }
1071
+ detectAdBlocker() {
1072
+ try {
1073
+ const bait = document.createElement("div");
1074
+ bait.className = "adsbox";
1075
+ bait.style.position = "absolute";
1076
+ bait.style.left = "-9999px";
1077
+ document.body.appendChild(bait);
1078
+ const blocked = bait.offsetHeight === 0 || bait.offsetWidth === 0;
1079
+ document.body.removeChild(bait);
1080
+ return blocked;
1081
+ } catch {
1082
+ return false;
1083
+ }
1084
+ }
1085
+ computeSimpleHash(value) {
1086
+ let hash = 0;
1087
+ for (let i = 0; i < value.length; i++) {
1088
+ hash = (hash << 5) - hash + value.charCodeAt(i) | 0;
1089
+ }
1090
+ return Math.abs(hash).toString(16);
1091
+ }
1092
+ actionFromRisk(riskScore) {
1093
+ if (riskScore >= 85) return "block";
1094
+ if (riskScore >= 65) return "hard_challenge";
1095
+ if (riskScore >= 40) return "soft_challenge";
1096
+ return "allow";
1097
+ }
787
1098
  /**
788
- * Get default endpoint
1099
+ * Resolve endpoint: config > env > production default.
789
1100
  */
790
1101
  getDefaultEndpoint() {
1102
+ const configuredEndpoint = this.normalizeEndpoint(this.config?.endpoint);
1103
+ if (configuredEndpoint) {
1104
+ return configuredEndpoint;
1105
+ }
1106
+ const envEndpoint = this.normalizeEndpoint(
1107
+ typeof globalThis !== "undefined" ? globalThis?.process?.env?.NEXT_PUBLIC_API_URL : void 0
1108
+ );
1109
+ if (envEndpoint) {
1110
+ return envEndpoint;
1111
+ }
791
1112
  return "https://api.keverd.com";
792
1113
  }
1114
+ normalizeEndpoint(value) {
1115
+ if (!value) return void 0;
1116
+ const trimmed = value.trim();
1117
+ if (!trimmed) return void 0;
1118
+ return trimmed.replace(/\/+$/, "");
1119
+ }
793
1120
  /**
794
1121
  * Generate a session ID
795
1122
  */
@@ -991,9 +1318,6 @@ var KeverdSDK = class {
991
1318
  * Destroy the SDK instance
992
1319
  */
993
1320
  async destroy() {
994
- if (this.sessionId) {
995
- await this.endSession();
996
- }
997
1321
  const wasDebug = this.config?.debug;
998
1322
  this.behavioralCollector.stop();
999
1323
  this.isInitialized = false;
@@ -1017,15 +1341,19 @@ var KeverdSDK = class {
1017
1341
  }
1018
1342
  };
1019
1343
  var KeverdContext = createContext(null);
1020
- function KeverdProvider({ loadOptions, children }) {
1344
+ function KeverdProvider({ apiKey, endpoint, debug, loadOptions, children }) {
1021
1345
  const sdkRef = useRef(new KeverdSDK());
1022
1346
  const [isReady, setIsReady] = useState(false);
1023
1347
  useEffect(() => {
1024
1348
  const sdk = sdkRef.current;
1025
1349
  if (!sdk.isReady()) {
1350
+ const resolvedApiKey = apiKey ?? loadOptions?.apiKey;
1351
+ const resolvedEndpoint = endpoint ?? loadOptions?.endpoint;
1352
+ const resolvedDebug = debug ?? loadOptions?.debug ?? false;
1026
1353
  sdk.init({
1027
- apiKey: loadOptions.apiKey,
1028
- debug: loadOptions.debug || false
1354
+ apiKey: resolvedApiKey,
1355
+ endpoint: resolvedEndpoint,
1356
+ debug: resolvedDebug
1029
1357
  });
1030
1358
  setIsReady(true);
1031
1359
  }
@@ -1036,7 +1364,7 @@ function KeverdProvider({ loadOptions, children }) {
1036
1364
  setIsReady(false);
1037
1365
  }
1038
1366
  };
1039
- }, []);
1367
+ }, [apiKey, endpoint, debug, loadOptions]);
1040
1368
  const contextValue = {
1041
1369
  sdk: sdkRef.current,
1042
1370
  isReady
@@ -1103,7 +1431,104 @@ function useKeverdVisitorData(options) {
1103
1431
  getData
1104
1432
  };
1105
1433
  }
1434
+ function useKeverd() {
1435
+ const { isLoading, error, data, getData } = useKeverdVisitorData({ immediate: true });
1436
+ useEffect(() => {
1437
+ if (typeof window === "undefined") return;
1438
+ if (data?.visitorId) {
1439
+ window.__KEVERD_DEVICE_ID__ = data.visitorId;
1440
+ }
1441
+ }, [data?.visitorId]);
1442
+ const refresh = async () => {
1443
+ await getData({ ignoreCache: true });
1444
+ };
1445
+ return useMemo(
1446
+ () => ({
1447
+ deviceId: data?.visitorId || null,
1448
+ riskScore: typeof data?.riskScore === "number" ? data.riskScore : null,
1449
+ isLoading,
1450
+ error,
1451
+ refresh
1452
+ }),
1453
+ [data?.visitorId, data?.riskScore, isLoading, error]
1454
+ );
1455
+ }
1456
+
1457
+ // src/utils/identifiers.ts
1458
+ function bufferToHex(buffer) {
1459
+ const bytes = new Uint8Array(buffer);
1460
+ let hex = "";
1461
+ for (let i = 0; i < bytes.length; i++) {
1462
+ hex += bytes[i].toString(16).padStart(2, "0");
1463
+ }
1464
+ return hex;
1465
+ }
1466
+ function fallbackHash(input) {
1467
+ let hash = 0;
1468
+ for (let i = 0; i < input.length; i++) {
1469
+ hash = (hash << 5) - hash + input.charCodeAt(i);
1470
+ hash |= 0;
1471
+ }
1472
+ return Math.abs(hash).toString(16);
1473
+ }
1474
+ async function hashLoginIdentifier(identifier, options) {
1475
+ if (!identifier) {
1476
+ throw new Error("[Keverd SDK] identifier is required for hashing");
1477
+ }
1478
+ const salt = options?.salt ?? "";
1479
+ const normalized = (salt + identifier).toLowerCase();
1480
+ const prefix = options?.prefix ?? "sha256";
1481
+ try {
1482
+ if (typeof window !== "undefined" && window.crypto?.subtle) {
1483
+ const encoder = new TextEncoder();
1484
+ const digest = await window.crypto.subtle.digest("SHA-256", encoder.encode(normalized));
1485
+ return `${prefix}:${bufferToHex(digest)}`;
1486
+ }
1487
+ } catch {
1488
+ }
1489
+ return `${prefix}:${fallbackHash(normalized)}`;
1490
+ }
1491
+ async function buildLoginContextFromIdentifier(options) {
1492
+ const identifierHash = await hashLoginIdentifier(options.identifier, {
1493
+ salt: options.salt
1494
+ });
1495
+ return {
1496
+ identifierHash,
1497
+ result: options.result,
1498
+ failureReason: options.failureReason,
1499
+ authMethod: options.authMethod,
1500
+ mfaUsed: options.mfaUsed,
1501
+ attemptId: options.attemptId
1502
+ };
1503
+ }
1504
+
1505
+ // src/utils/adaptive.ts
1506
+ function handleAdaptiveResponse(response, handlers) {
1507
+ const anyResp = response;
1508
+ const adaptive = anyResp.adaptive_response || {};
1509
+ const recommendedAction = anyResp.recommended_action || adaptive.recommended_action || response.action;
1510
+ const challenges = Array.isArray(adaptive.challenges) ? adaptive.challenges : [];
1511
+ switch (recommendedAction) {
1512
+ case "allow":
1513
+ handlers.onAllow?.(response);
1514
+ break;
1515
+ case "soft_challenge":
1516
+ handlers.onSoftChallenge?.(response);
1517
+ break;
1518
+ case "hard_challenge":
1519
+ handlers.onHardChallenge?.(response);
1520
+ break;
1521
+ case "block":
1522
+ handlers.onBlock?.(response);
1523
+ break;
1524
+ default:
1525
+ handlers.onUnknown?.(response);
1526
+ }
1527
+ if (challenges.length) {
1528
+ handlers.onChallenges?.(response, challenges);
1529
+ }
1530
+ }
1106
1531
 
1107
- export { KeverdBehavioralCollector, KeverdDeviceCollector, KeverdProvider, KeverdSDK, useKeverdContext, useKeverdVisitorData };
1532
+ export { KeverdBehavioralCollector, KeverdDeviceCollector, KeverdProvider, KeverdSDK, buildLoginContextFromIdentifier, handleAdaptiveResponse, hashLoginIdentifier, useKeverd, useKeverdContext, useKeverdVisitorData };
1108
1533
  //# sourceMappingURL=index.mjs.map
1109
1534
  //# sourceMappingURL=index.mjs.map