@keverdjs/fraud-sdk-react 2.0.0 → 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/README.md +24 -38
- package/dist/index.d.mts +132 -12
- package/dist/index.d.ts +132 -12
- package/dist/index.js +484 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +482 -39
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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,39 +695,69 @@ 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
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Extract origin and referrer from browser
|
|
721
|
+
*/
|
|
722
|
+
getOriginHeaders() {
|
|
723
|
+
if (typeof window === "undefined") {
|
|
724
|
+
return {};
|
|
725
|
+
}
|
|
726
|
+
const origin = window.location.origin || void 0;
|
|
727
|
+
const referrer = document.referrer || void 0;
|
|
728
|
+
return { origin, referrer };
|
|
729
|
+
}
|
|
703
730
|
/**
|
|
704
731
|
* Send fingerprint request to backend
|
|
705
732
|
*/
|
|
706
|
-
async sendFingerprintRequest(request, options) {
|
|
733
|
+
async sendFingerprintRequest(request, options, privacySignals) {
|
|
707
734
|
if (!this.config) {
|
|
708
735
|
throw new Error("SDK not initialized");
|
|
709
736
|
}
|
|
710
|
-
const url = `${this.getDefaultEndpoint()}/fingerprint
|
|
737
|
+
const url = `${this.getDefaultEndpoint()}/v2/fingerprint`;
|
|
711
738
|
const headers = {
|
|
712
739
|
"Content-Type": "application/json",
|
|
713
740
|
"X-SDK-Source": "react"
|
|
714
741
|
// Identify SDK source for backend analytics
|
|
715
742
|
};
|
|
743
|
+
const { origin, referrer } = this.getOriginHeaders();
|
|
744
|
+
if (origin) {
|
|
745
|
+
headers["X-Origin-URL"] = origin;
|
|
746
|
+
}
|
|
747
|
+
if (referrer) {
|
|
748
|
+
headers["X-Referrer-URL"] = referrer;
|
|
749
|
+
}
|
|
716
750
|
const apiKey = this.config.apiKey;
|
|
717
751
|
if (apiKey) {
|
|
718
752
|
headers["x-keverd-key"] = apiKey;
|
|
719
753
|
headers["X-API-KEY"] = apiKey;
|
|
720
754
|
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
721
755
|
}
|
|
722
|
-
|
|
723
|
-
headers["X-Sandbox"] = "true";
|
|
724
|
-
}
|
|
725
|
-
const requestBody = {
|
|
726
|
-
...request,
|
|
727
|
-
session: {
|
|
728
|
-
...request.session || {},
|
|
729
|
-
sessionId: this.sessionId || request.session?.sessionId
|
|
730
|
-
}
|
|
731
|
-
};
|
|
756
|
+
const signalsPayload = this.buildV2SignalsPayload(request, privacySignals);
|
|
732
757
|
const response = await fetch(url, {
|
|
733
758
|
method: "POST",
|
|
734
759
|
headers,
|
|
735
|
-
body: JSON.stringify(
|
|
760
|
+
body: JSON.stringify({ signals: signalsPayload })
|
|
736
761
|
});
|
|
737
762
|
if (!response.ok) {
|
|
738
763
|
const errorText = await response.text().catch(() => "Unknown error");
|
|
@@ -753,25 +778,345 @@ var KeverdSDK = class {
|
|
|
753
778
|
* Transform API response to visitor data format
|
|
754
779
|
*/
|
|
755
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
|
+
}
|
|
756
821
|
return {
|
|
757
|
-
visitorId: response.requestId,
|
|
758
|
-
riskScore: response.risk_score,
|
|
759
|
-
score: response.score,
|
|
760
|
-
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",
|
|
761
826
|
reasons: response.reason || [],
|
|
762
|
-
sessionId: response.session_id,
|
|
763
|
-
requestId: response.requestId,
|
|
827
|
+
sessionId: response.session_id || this.sessionId || this.generateSessionId(),
|
|
828
|
+
requestId: response.requestId || this.sessionId || this.generateSessionId(),
|
|
764
829
|
simSwapEngine: response.sim_swap_engine,
|
|
765
|
-
confidence: 1 - response.score
|
|
766
|
-
//
|
|
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
|
|
1034
|
+
};
|
|
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()
|
|
767
1059
|
};
|
|
768
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
|
+
}
|
|
769
1098
|
/**
|
|
770
|
-
*
|
|
1099
|
+
* Resolve endpoint: config > env > production default.
|
|
771
1100
|
*/
|
|
772
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
|
+
}
|
|
773
1112
|
return "https://api.keverd.com";
|
|
774
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
|
+
}
|
|
775
1120
|
/**
|
|
776
1121
|
* Generate a session ID
|
|
777
1122
|
*/
|
|
@@ -973,9 +1318,6 @@ var KeverdSDK = class {
|
|
|
973
1318
|
* Destroy the SDK instance
|
|
974
1319
|
*/
|
|
975
1320
|
async destroy() {
|
|
976
|
-
if (this.sessionId) {
|
|
977
|
-
await this.endSession();
|
|
978
|
-
}
|
|
979
1321
|
const wasDebug = this.config?.debug;
|
|
980
1322
|
this.behavioralCollector.stop();
|
|
981
1323
|
this.isInitialized = false;
|
|
@@ -999,15 +1341,19 @@ var KeverdSDK = class {
|
|
|
999
1341
|
}
|
|
1000
1342
|
};
|
|
1001
1343
|
var KeverdContext = createContext(null);
|
|
1002
|
-
function KeverdProvider({ loadOptions, children }) {
|
|
1344
|
+
function KeverdProvider({ apiKey, endpoint, debug, loadOptions, children }) {
|
|
1003
1345
|
const sdkRef = useRef(new KeverdSDK());
|
|
1004
1346
|
const [isReady, setIsReady] = useState(false);
|
|
1005
1347
|
useEffect(() => {
|
|
1006
1348
|
const sdk = sdkRef.current;
|
|
1007
1349
|
if (!sdk.isReady()) {
|
|
1350
|
+
const resolvedApiKey = apiKey ?? loadOptions?.apiKey;
|
|
1351
|
+
const resolvedEndpoint = endpoint ?? loadOptions?.endpoint;
|
|
1352
|
+
const resolvedDebug = debug ?? loadOptions?.debug ?? false;
|
|
1008
1353
|
sdk.init({
|
|
1009
|
-
apiKey:
|
|
1010
|
-
|
|
1354
|
+
apiKey: resolvedApiKey,
|
|
1355
|
+
endpoint: resolvedEndpoint,
|
|
1356
|
+
debug: resolvedDebug
|
|
1011
1357
|
});
|
|
1012
1358
|
setIsReady(true);
|
|
1013
1359
|
}
|
|
@@ -1018,7 +1364,7 @@ function KeverdProvider({ loadOptions, children }) {
|
|
|
1018
1364
|
setIsReady(false);
|
|
1019
1365
|
}
|
|
1020
1366
|
};
|
|
1021
|
-
}, []);
|
|
1367
|
+
}, [apiKey, endpoint, debug, loadOptions]);
|
|
1022
1368
|
const contextValue = {
|
|
1023
1369
|
sdk: sdkRef.current,
|
|
1024
1370
|
isReady
|
|
@@ -1085,7 +1431,104 @@ function useKeverdVisitorData(options) {
|
|
|
1085
1431
|
getData
|
|
1086
1432
|
};
|
|
1087
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
|
+
}
|
|
1088
1531
|
|
|
1089
|
-
export { KeverdBehavioralCollector, KeverdDeviceCollector, KeverdProvider, KeverdSDK, useKeverdContext, useKeverdVisitorData };
|
|
1532
|
+
export { KeverdBehavioralCollector, KeverdDeviceCollector, KeverdProvider, KeverdSDK, buildLoginContextFromIdentifier, handleAdaptiveResponse, hashLoginIdentifier, useKeverd, useKeverdContext, useKeverdVisitorData };
|
|
1090
1533
|
//# sourceMappingURL=index.mjs.map
|
|
1091
1534
|
//# sourceMappingURL=index.mjs.map
|