vesant-sdk 1.7.0-dev.f9faca4 → 1.7.0-dev.fe84882
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/{client-DoMSYMMR.d.ts → client-0NOPDnT0.d.ts} +29 -26
- package/dist/{client-DMIRx7Tu.d.mts → client-CvWNRwwg.d.mts} +29 -26
- package/dist/{client-ZNdnpWe7.d.mts → client-Dps40EtF.d.mts} +2 -2
- package/dist/{client-C3DCmGe9.d.ts → client-SMxqod4j.d.ts} +2 -2
- package/dist/{client-BolQlL5e.d.mts → client-q9fN8Hhf.d.mts} +103 -1
- package/dist/{client-BolQlL5e.d.ts → client-q9fN8Hhf.d.ts} +103 -1
- package/dist/compliance/index.d.mts +3 -3
- package/dist/compliance/index.d.ts +3 -3
- package/dist/compliance/index.js +133 -22
- package/dist/compliance/index.js.map +1 -1
- package/dist/compliance/index.mjs +133 -22
- package/dist/compliance/index.mjs.map +1 -1
- package/dist/decisions/index.d.mts +1 -1
- package/dist/decisions/index.d.ts +1 -1
- package/dist/decisions/index.js +68 -7
- package/dist/decisions/index.js.map +1 -1
- package/dist/decisions/index.mjs +68 -7
- package/dist/decisions/index.mjs.map +1 -1
- package/dist/geolocation/index.d.mts +59 -4
- package/dist/geolocation/index.d.ts +59 -4
- package/dist/geolocation/index.js +136 -19
- package/dist/geolocation/index.js.map +1 -1
- package/dist/geolocation/index.mjs +134 -20
- package/dist/geolocation/index.mjs.map +1 -1
- package/dist/index-C3zUKydT.d.mts +264 -0
- package/dist/index-DSDgS09k.d.ts +264 -0
- package/dist/index.d.mts +26 -9
- package/dist/index.d.ts +26 -9
- package/dist/index.js +325 -58
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +318 -59
- package/dist/index.mjs.map +1 -1
- package/dist/kyc/core.d.mts +2 -2
- package/dist/kyc/core.d.ts +2 -2
- package/dist/kyc/core.js +94 -19
- package/dist/kyc/core.js.map +1 -1
- package/dist/kyc/core.mjs +94 -19
- package/dist/kyc/core.mjs.map +1 -1
- package/dist/kyc/index.d.mts +110 -13
- package/dist/kyc/index.d.ts +110 -13
- package/dist/kyc/index.js +94 -19
- package/dist/kyc/index.js.map +1 -1
- package/dist/kyc/index.mjs +94 -19
- package/dist/kyc/index.mjs.map +1 -1
- package/dist/react.d.mts +9 -3
- package/dist/react.d.ts +9 -3
- package/dist/react.js +180 -77
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +180 -77
- package/dist/react.mjs.map +1 -1
- package/dist/risk-profile/index.d.mts +1 -1
- package/dist/risk-profile/index.d.ts +1 -1
- package/dist/risk-profile/index.js +68 -7
- package/dist/risk-profile/index.js.map +1 -1
- package/dist/risk-profile/index.mjs +68 -7
- package/dist/risk-profile/index.mjs.map +1 -1
- package/dist/scores/index.d.mts +1 -1
- package/dist/scores/index.d.ts +1 -1
- package/dist/scores/index.js +68 -7
- package/dist/scores/index.js.map +1 -1
- package/dist/scores/index.mjs +68 -7
- package/dist/scores/index.mjs.map +1 -1
- package/dist/tax/index.d.mts +2 -2
- package/dist/tax/index.d.ts +2 -2
- package/dist/tax/index.js +74 -10
- package/dist/tax/index.js.map +1 -1
- package/dist/tax/index.mjs +74 -10
- package/dist/tax/index.mjs.map +1 -1
- package/dist/webhooks/index.d.mts +2 -189
- package/dist/webhooks/index.d.ts +2 -189
- package/dist/webhooks/index.js +85 -21
- package/dist/webhooks/index.js.map +1 -1
- package/dist/webhooks/index.mjs +85 -22
- package/dist/webhooks/index.mjs.map +1 -1
- package/package.json +4 -1
package/dist/react.mjs
CHANGED
|
@@ -87,6 +87,61 @@ function getBrowserInfo() {
|
|
|
87
87
|
return { browser, browser_version: browserVersion, os, os_version: osVersion };
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// src/geolocation/integrity.ts
|
|
91
|
+
var isNil = (v) => v === null || v === void 0;
|
|
92
|
+
function assessGpsIntegrity(input, opts = {}) {
|
|
93
|
+
const flags = [];
|
|
94
|
+
const now = opts.now ?? (() => Date.now());
|
|
95
|
+
const maxAgeMs = opts.maxAgeMs ?? 6e4;
|
|
96
|
+
const minAccuracy = opts.minAccuracyMeters ?? 1;
|
|
97
|
+
const { accuracy, altitude, altitudeAccuracy, heading, speed, timestamp } = input;
|
|
98
|
+
if (isNil(accuracy)) {
|
|
99
|
+
flags.push("accuracy_missing");
|
|
100
|
+
} else if (accuracy === 0) {
|
|
101
|
+
flags.push("accuracy_zero");
|
|
102
|
+
} else if (accuracy < minAccuracy) {
|
|
103
|
+
flags.push("accuracy_implausibly_small");
|
|
104
|
+
}
|
|
105
|
+
const hasMotion = !isNil(heading) || !isNil(speed);
|
|
106
|
+
if (hasMotion && (isNil(accuracy) || accuracy === 0)) {
|
|
107
|
+
flags.push("motion_without_accuracy");
|
|
108
|
+
}
|
|
109
|
+
if (!isNil(altitude) && isNil(altitudeAccuracy)) {
|
|
110
|
+
flags.push("altitude_without_accuracy");
|
|
111
|
+
}
|
|
112
|
+
if (timestamp !== void 0 && now() - timestamp > maxAgeMs) {
|
|
113
|
+
flags.push("stale_timestamp");
|
|
114
|
+
}
|
|
115
|
+
if (opts.permissionState === "denied" || opts.permissionState === "prompt") {
|
|
116
|
+
flags.push(`permission_${opts.permissionState}`);
|
|
117
|
+
}
|
|
118
|
+
return { suspicious: flags.length > 0, flags };
|
|
119
|
+
}
|
|
120
|
+
function gpsIntegrityInputFromPosition(position) {
|
|
121
|
+
const c = position.coords;
|
|
122
|
+
return {
|
|
123
|
+
latitude: c.latitude,
|
|
124
|
+
longitude: c.longitude,
|
|
125
|
+
accuracy: c.accuracy,
|
|
126
|
+
altitude: c.altitude,
|
|
127
|
+
altitudeAccuracy: c.altitudeAccuracy,
|
|
128
|
+
heading: c.heading,
|
|
129
|
+
speed: c.speed,
|
|
130
|
+
timestamp: position.timestamp
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async function probeGeolocationPermission() {
|
|
134
|
+
try {
|
|
135
|
+
if (typeof navigator === "undefined" || !navigator.permissions?.query) {
|
|
136
|
+
return "unavailable";
|
|
137
|
+
}
|
|
138
|
+
const status = await navigator.permissions.query({ name: "geolocation" });
|
|
139
|
+
return status.state;
|
|
140
|
+
} catch {
|
|
141
|
+
return "unavailable";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
90
145
|
// src/geolocation/ciphertext.ts
|
|
91
146
|
var CIPHER_TEXT_EXPIRY_MINUTES = 5;
|
|
92
147
|
async function computeHMAC(key, message) {
|
|
@@ -139,6 +194,7 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
|
|
|
139
194
|
if (typeof navigator === "undefined" || !navigator.geolocation) {
|
|
140
195
|
return null;
|
|
141
196
|
}
|
|
197
|
+
const permissionState = await probeGeolocationPermission();
|
|
142
198
|
return new Promise((resolve) => {
|
|
143
199
|
navigator.geolocation.getCurrentPosition(
|
|
144
200
|
(position) => {
|
|
@@ -155,6 +211,7 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
|
|
|
155
211
|
resolve(null);
|
|
156
212
|
return;
|
|
157
213
|
}
|
|
214
|
+
const { flags } = assessGpsIntegrity(gpsIntegrityInputFromPosition(position), { permissionState });
|
|
158
215
|
resolve({
|
|
159
216
|
latitude,
|
|
160
217
|
longitude,
|
|
@@ -163,7 +220,8 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
|
|
|
163
220
|
altitude_accuracy: position.coords.altitudeAccuracy ?? void 0,
|
|
164
221
|
heading: position.coords.heading ?? void 0,
|
|
165
222
|
speed: position.coords.speed ?? void 0,
|
|
166
|
-
timestamp: position.timestamp
|
|
223
|
+
timestamp: position.timestamp,
|
|
224
|
+
integrity_flags: flags.length ? flags : void 0
|
|
167
225
|
});
|
|
168
226
|
},
|
|
169
227
|
() => {
|
|
@@ -230,6 +288,9 @@ async function generateCipherText(options, config) {
|
|
|
230
288
|
);
|
|
231
289
|
if (location) {
|
|
232
290
|
locationData = location;
|
|
291
|
+
if (location.integrity_flags?.length) {
|
|
292
|
+
warnings.push(`GPS integrity flags (advisory): ${location.integrity_flags.join(", ")}`);
|
|
293
|
+
}
|
|
233
294
|
} else if (gpsRequiredByConfig) {
|
|
234
295
|
throw new VesantError(
|
|
235
296
|
`GPS location is required for ${options.reason} by tenant configuration, but GPS was not available or permission was denied`,
|
|
@@ -332,8 +393,6 @@ function useGeolocation(client, options = {}) {
|
|
|
332
393
|
if (optionsRef.current.autoVerify) {
|
|
333
394
|
const detectAndVerify = async () => {
|
|
334
395
|
try {
|
|
335
|
-
const ipResponse = await fetch("https://api.ipify.org?format=json");
|
|
336
|
-
const { ip } = await ipResponse.json();
|
|
337
396
|
let deviceFingerprint;
|
|
338
397
|
if (optionsRef.current.includeDeviceFingerprint && typeof window !== "undefined") {
|
|
339
398
|
const deviceId = generateDeviceId();
|
|
@@ -347,7 +406,6 @@ function useGeolocation(client, options = {}) {
|
|
|
347
406
|
};
|
|
348
407
|
}
|
|
349
408
|
await verifyIP({
|
|
350
|
-
ip_address: ip,
|
|
351
409
|
user_id: optionsRef.current.userId || "",
|
|
352
410
|
event_type: optionsRef.current.eventType || "page_view",
|
|
353
411
|
device_fingerprint: deviceFingerprint
|
|
@@ -661,6 +719,12 @@ function useLocationCapture(client, options) {
|
|
|
661
719
|
if (accuracy !== null && accuracy !== void 0 && accuracy > 0) {
|
|
662
720
|
capture.accuracy = accuracy;
|
|
663
721
|
}
|
|
722
|
+
const { flags } = assessGpsIntegrity(gpsIntegrityInputFromPosition(position), {
|
|
723
|
+
permissionState: await probeGeolocationPermission()
|
|
724
|
+
});
|
|
725
|
+
if (flags.length) {
|
|
726
|
+
capture.integrity_flags = flags;
|
|
727
|
+
}
|
|
664
728
|
}
|
|
665
729
|
}
|
|
666
730
|
if (gpsError) {
|
|
@@ -883,6 +947,87 @@ var Camera = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0i
|
|
|
883
947
|
var Done = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik04LjUgMTIuNUwxMC41IDE0LjVMMTUuNSA5LjUiIHN0cm9rZT0iIzFDMjc0QyIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPg0KPHBhdGggZD0iTTcgMy4zMzc4MkM4LjQ3MDg3IDIuNDg2OTcgMTAuMTc4NiAyIDEyIDJDMTcuNTIyOCAyIDIyIDYuNDc3MTUgMjIgMTJDMjIgMTcuNTIyOCAxNy41MjI4IDIyIDEyIDIyQzYuNDc3MTUgMjIgMiAxNy41MjI4IDIgMTJDMiAxMC4xNzg2IDIuNDg2OTcgOC40NzA4NyAzLjMzNzgyIDciIHN0cm9rZT0iIzFDMjc0QyIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPg0KPC9zdmc+";
|
|
884
948
|
var Close = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik0xNC41IDkuNTAwMDJMOS41IDE0LjVNOS40OTk5OCA5LjVMMTQuNSAxNC41IiBzdHJva2U9IiMxQzI3NEMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4NCjxwYXRoIGQ9Ik03IDMuMzM3ODJDOC40NzA4NyAyLjQ4Njk3IDEwLjE3ODYgMiAxMiAyQzE3LjUyMjggMiAyMiA2LjQ3NzE1IDIyIDEyQzIyIDE3LjUyMjggMTcuNTIyOCAyMiAxMiAyMkM2LjQ3NzE1IDIyIDIgMTcuNTIyOCAyIDEyQzIgMTAuMTc4NiAyLjQ4Njk3IDguNDcwODcgMy4zMzc4MiA3IiBzdHJva2U9IiMxQzI3NEMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4NCjwvc3ZnPg==";
|
|
885
949
|
|
|
950
|
+
// src/kyc/faceDetection.ts
|
|
951
|
+
var FACE_MESSAGES = {
|
|
952
|
+
idle: "Initializing camera...",
|
|
953
|
+
loading: "Loading face detection...",
|
|
954
|
+
scanning: "Position your face in the circle",
|
|
955
|
+
too_far: "Face too far. Please move closer to the camera",
|
|
956
|
+
too_close: "Too close. Please move back a little",
|
|
957
|
+
off_center: "Center your face in the circle",
|
|
958
|
+
ok: "Looking great! Tap the button to capture"
|
|
959
|
+
};
|
|
960
|
+
var FACE_COLORS = {
|
|
961
|
+
idle: "#9ca3af",
|
|
962
|
+
loading: "#3b82f6",
|
|
963
|
+
scanning: "#3b82f6",
|
|
964
|
+
too_far: "#ef4444",
|
|
965
|
+
too_close: "#f59e0b",
|
|
966
|
+
off_center: "#f59e0b",
|
|
967
|
+
ok: "#10b981"
|
|
968
|
+
};
|
|
969
|
+
var MEDIAPIPE_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/face_detection";
|
|
970
|
+
function mapDetectionsToFaceStatus(detections) {
|
|
971
|
+
if (detections.length === 0) return "scanning";
|
|
972
|
+
const bb = detections[0].boundingBox;
|
|
973
|
+
if (bb.width < 0.18) return "too_far";
|
|
974
|
+
if (bb.width > 0.72) return "too_close";
|
|
975
|
+
if (bb.xCenter < 0.28 || bb.xCenter > 0.72 || bb.yCenter < 0.28 || bb.yCenter > 0.72) {
|
|
976
|
+
return "off_center";
|
|
977
|
+
}
|
|
978
|
+
return "ok";
|
|
979
|
+
}
|
|
980
|
+
function isCaptureDisabled(ready, detectionActive, faceStatus) {
|
|
981
|
+
return !ready || detectionActive && faceStatus !== "ok";
|
|
982
|
+
}
|
|
983
|
+
function resolveMediaPipeBase(base) {
|
|
984
|
+
if (!base) return MEDIAPIPE_BASE;
|
|
985
|
+
return base.endsWith("/") ? base.slice(0, -1) : base;
|
|
986
|
+
}
|
|
987
|
+
var LIVENESS_SAMPLE_LIMIT = 20;
|
|
988
|
+
function buildLivenessArtifact(samples) {
|
|
989
|
+
if (samples.length === 0) return void 0;
|
|
990
|
+
return JSON.stringify({
|
|
991
|
+
version: 1,
|
|
992
|
+
source: "mediapipe-face-detection",
|
|
993
|
+
samples: samples.slice(-LIVENESS_SAMPLE_LIMIT)
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
var mediapipeLoaderPromises = /* @__PURE__ */ new Map();
|
|
997
|
+
function loadMediaPipeFaceDetection(assetBasePath) {
|
|
998
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
999
|
+
return Promise.reject(new Error("MediaPipe requires a browser environment"));
|
|
1000
|
+
}
|
|
1001
|
+
const w = window;
|
|
1002
|
+
if (w.FaceDetection) {
|
|
1003
|
+
return Promise.resolve(w.FaceDetection);
|
|
1004
|
+
}
|
|
1005
|
+
const src = `${resolveMediaPipeBase(assetBasePath)}/face_detection.js`;
|
|
1006
|
+
const existing = mediapipeLoaderPromises.get(src);
|
|
1007
|
+
if (existing) return existing;
|
|
1008
|
+
const loader = new Promise((resolve, reject) => {
|
|
1009
|
+
const script = document.createElement("script");
|
|
1010
|
+
script.src = src;
|
|
1011
|
+
script.async = true;
|
|
1012
|
+
script.crossOrigin = "anonymous";
|
|
1013
|
+
script.onload = () => {
|
|
1014
|
+
if (w.FaceDetection) {
|
|
1015
|
+
resolve(w.FaceDetection);
|
|
1016
|
+
} else {
|
|
1017
|
+
mediapipeLoaderPromises.delete(src);
|
|
1018
|
+
reject(new Error("MediaPipe loaded but FaceDetection global is missing"));
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
script.onerror = () => {
|
|
1022
|
+
mediapipeLoaderPromises.delete(src);
|
|
1023
|
+
reject(new Error("Failed to load MediaPipe face_detection script"));
|
|
1024
|
+
};
|
|
1025
|
+
document.head.appendChild(script);
|
|
1026
|
+
});
|
|
1027
|
+
mediapipeLoaderPromises.set(src, loader);
|
|
1028
|
+
return loader;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
886
1031
|
// src/kyc/FaceCaptureModal.tsx
|
|
887
1032
|
var MOBILE_UA = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
|
|
888
1033
|
var SESSION_EXPIRY_MS = 15 * 60 * 1e3;
|
|
@@ -916,7 +1061,8 @@ function FaceCaptureModal({
|
|
|
916
1061
|
onComplete,
|
|
917
1062
|
onCancel,
|
|
918
1063
|
defaultDevice,
|
|
919
|
-
renderQR
|
|
1064
|
+
renderQR,
|
|
1065
|
+
mediapipeAssetBasePath
|
|
920
1066
|
}) {
|
|
921
1067
|
const isMobile = typeof navigator !== "undefined" && MOBILE_UA.test(navigator.userAgent);
|
|
922
1068
|
const initialChoice = defaultDevice ?? (isMobile ? "this" : "ask");
|
|
@@ -933,6 +1079,7 @@ function FaceCaptureModal({
|
|
|
933
1079
|
const [captureMode, setCaptureMode] = useState("idle");
|
|
934
1080
|
const [capturedPreview, setCapturedPreview] = useState(null);
|
|
935
1081
|
const [capturedBase64, setCapturedBase64] = useState(null);
|
|
1082
|
+
const [capturedArtifact, setCapturedArtifact] = useState(null);
|
|
936
1083
|
const [livenessMessage, setLivenessMessage] = useState(LIVENESS_MESSAGES[0]);
|
|
937
1084
|
const qrPayload = session.link || `vesant://reuse-kyc?token=${encodeURIComponent(session.token)}`;
|
|
938
1085
|
const cancelledRef = useRef(false);
|
|
@@ -1030,7 +1177,10 @@ function FaceCaptureModal({
|
|
|
1030
1177
|
const data = await client.submitEventBasedFaceVerificationSession({
|
|
1031
1178
|
token: session.token,
|
|
1032
1179
|
reference: ref,
|
|
1033
|
-
proof
|
|
1180
|
+
proof,
|
|
1181
|
+
// undefined when detection wasn't running for this capture —
|
|
1182
|
+
// JSON.stringify drops it, so the field is absent on the wire.
|
|
1183
|
+
liveness_artifact: capturedArtifact ?? void 0
|
|
1034
1184
|
});
|
|
1035
1185
|
return { ok: true, data };
|
|
1036
1186
|
} catch (err2) {
|
|
@@ -1076,16 +1226,18 @@ function FaceCaptureModal({
|
|
|
1076
1226
|
submittingRef.current = false;
|
|
1077
1227
|
}
|
|
1078
1228
|
};
|
|
1079
|
-
const handleCameraCapture = (dataUrl) => {
|
|
1229
|
+
const handleCameraCapture = (dataUrl, livenessArtifact) => {
|
|
1080
1230
|
const raw = dataUrl.split(",")[1] ?? "";
|
|
1081
1231
|
setCapturedPreview(dataUrl);
|
|
1082
1232
|
setCapturedBase64(raw);
|
|
1233
|
+
setCapturedArtifact(livenessArtifact ?? null);
|
|
1083
1234
|
setCaptureMode("preview");
|
|
1084
1235
|
};
|
|
1085
1236
|
const handleCameraCancel = () => setCaptureMode("idle");
|
|
1086
1237
|
const handleRetake = () => {
|
|
1087
1238
|
setCapturedPreview(null);
|
|
1088
1239
|
setCapturedBase64(null);
|
|
1240
|
+
setCapturedArtifact(null);
|
|
1089
1241
|
setCaptureMode("live");
|
|
1090
1242
|
};
|
|
1091
1243
|
const handleConfirmSubmit = () => {
|
|
@@ -1157,7 +1309,8 @@ function FaceCaptureModal({
|
|
|
1157
1309
|
{
|
|
1158
1310
|
onCapture: handleCameraCapture,
|
|
1159
1311
|
onCancel: handleCameraCancel,
|
|
1160
|
-
message: livenessMessage
|
|
1312
|
+
message: livenessMessage,
|
|
1313
|
+
assetBasePath: mediapipeAssetBasePath
|
|
1161
1314
|
}
|
|
1162
1315
|
), /* @__PURE__ */ React.createElement("p", { style: attemptsTextStyle }, "Attempt ", attempts + 1, " of ", maxAttempts));
|
|
1163
1316
|
}
|
|
@@ -1388,59 +1541,10 @@ var attemptsTextStyle = {
|
|
|
1388
1541
|
};
|
|
1389
1542
|
var footerStyle = { padding: "12px 24px 16px", borderTop: "1px solid #f3f4f6" };
|
|
1390
1543
|
var footerTextStyle = { margin: 0, fontSize: 12, color: "#9ca3af", textAlign: "center" };
|
|
1391
|
-
|
|
1392
|
-
idle: "Initializing camera...",
|
|
1393
|
-
loading: "Loading face detection...",
|
|
1394
|
-
scanning: "Position your face in the circle",
|
|
1395
|
-
too_far: "Face too far. Please move closer to the camera",
|
|
1396
|
-
too_close: "Too close. Please move back a little",
|
|
1397
|
-
off_center: "Center your face in the circle",
|
|
1398
|
-
ok: "Looking great! Tap the button to capture"
|
|
1399
|
-
};
|
|
1400
|
-
var FACE_COLORS = {
|
|
1401
|
-
idle: "#9ca3af",
|
|
1402
|
-
loading: "#3b82f6",
|
|
1403
|
-
scanning: "#3b82f6",
|
|
1404
|
-
too_far: "#ef4444",
|
|
1405
|
-
too_close: "#f59e0b",
|
|
1406
|
-
off_center: "#f59e0b",
|
|
1407
|
-
ok: "#10b981"
|
|
1408
|
-
};
|
|
1409
|
-
var MEDIAPIPE_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/face_detection";
|
|
1410
|
-
var mediapipeLoaderPromise = null;
|
|
1411
|
-
function loadMediaPipeFaceDetection() {
|
|
1412
|
-
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
1413
|
-
return Promise.reject(new Error("MediaPipe requires a browser environment"));
|
|
1414
|
-
}
|
|
1415
|
-
const w = window;
|
|
1416
|
-
if (w.FaceDetection) {
|
|
1417
|
-
return Promise.resolve(w.FaceDetection);
|
|
1418
|
-
}
|
|
1419
|
-
if (mediapipeLoaderPromise) return mediapipeLoaderPromise;
|
|
1420
|
-
mediapipeLoaderPromise = new Promise((resolve, reject) => {
|
|
1421
|
-
const script = document.createElement("script");
|
|
1422
|
-
script.src = `${MEDIAPIPE_BASE}/face_detection.js`;
|
|
1423
|
-
script.async = true;
|
|
1424
|
-
script.crossOrigin = "anonymous";
|
|
1425
|
-
script.onload = () => {
|
|
1426
|
-
if (w.FaceDetection) {
|
|
1427
|
-
resolve(w.FaceDetection);
|
|
1428
|
-
} else {
|
|
1429
|
-
mediapipeLoaderPromise = null;
|
|
1430
|
-
reject(new Error("MediaPipe loaded but FaceDetection global is missing"));
|
|
1431
|
-
}
|
|
1432
|
-
};
|
|
1433
|
-
script.onerror = () => {
|
|
1434
|
-
mediapipeLoaderPromise = null;
|
|
1435
|
-
reject(new Error("Failed to load MediaPipe face_detection script"));
|
|
1436
|
-
};
|
|
1437
|
-
document.head.appendChild(script);
|
|
1438
|
-
});
|
|
1439
|
-
return mediapipeLoaderPromise;
|
|
1440
|
-
}
|
|
1441
|
-
function LiveCamera({ onCapture, onCancel, message }) {
|
|
1544
|
+
function LiveCamera({ onCapture, onCancel, message, assetBasePath }) {
|
|
1442
1545
|
const videoRef = useRef(null);
|
|
1443
1546
|
const streamRef = useRef(null);
|
|
1547
|
+
const samplesRef = useRef([]);
|
|
1444
1548
|
const [ready, setReady] = useState(false);
|
|
1445
1549
|
const [error, setError] = useState(null);
|
|
1446
1550
|
const [faceStatus, setFaceStatus] = useState("idle");
|
|
@@ -1495,31 +1599,29 @@ function LiveCamera({ onCapture, onCancel, message }) {
|
|
|
1495
1599
|
let detector = null;
|
|
1496
1600
|
setFaceStatus("loading");
|
|
1497
1601
|
setFaceDetectionFailed(false);
|
|
1602
|
+
samplesRef.current = [];
|
|
1498
1603
|
(async () => {
|
|
1499
1604
|
try {
|
|
1500
|
-
const FaceDetection = await loadMediaPipeFaceDetection();
|
|
1605
|
+
const FaceDetection = await loadMediaPipeFaceDetection(assetBasePath);
|
|
1501
1606
|
if (cancelled) return;
|
|
1607
|
+
const assetBase = resolveMediaPipeBase(assetBasePath);
|
|
1502
1608
|
const d = new FaceDetection({
|
|
1503
|
-
locateFile: (f) => `${
|
|
1609
|
+
locateFile: (f) => `${assetBase}/${f}`
|
|
1504
1610
|
});
|
|
1505
1611
|
d.setOptions({ model: "short", minDetectionConfidence: 0.5 });
|
|
1506
1612
|
d.onResults((results) => {
|
|
1507
1613
|
if (cancelled) return;
|
|
1508
1614
|
const detections = results.detections ?? [];
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
next = "off_center";
|
|
1518
|
-
} else {
|
|
1519
|
-
next = "ok";
|
|
1520
|
-
}
|
|
1615
|
+
const status = mapDetectionsToFaceStatus(detections);
|
|
1616
|
+
samplesRef.current.push({
|
|
1617
|
+
t: Date.now(),
|
|
1618
|
+
status,
|
|
1619
|
+
...detections.length > 0 ? { boundingBox: detections[0].boundingBox } : {}
|
|
1620
|
+
});
|
|
1621
|
+
if (samplesRef.current.length > LIVENESS_SAMPLE_LIMIT) {
|
|
1622
|
+
samplesRef.current.shift();
|
|
1521
1623
|
}
|
|
1522
|
-
setFaceStatus(
|
|
1624
|
+
setFaceStatus(status);
|
|
1523
1625
|
});
|
|
1524
1626
|
await d.initialize();
|
|
1525
1627
|
if (cancelled) return;
|
|
@@ -1555,7 +1657,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
|
|
|
1555
1657
|
} catch {
|
|
1556
1658
|
}
|
|
1557
1659
|
};
|
|
1558
|
-
}, [ready]);
|
|
1660
|
+
}, [ready, assetBasePath]);
|
|
1559
1661
|
const capture = () => {
|
|
1560
1662
|
const video = videoRef.current;
|
|
1561
1663
|
if (!video || !video.videoWidth) return;
|
|
@@ -1572,7 +1674,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
|
|
|
1572
1674
|
ctx.drawImage(video, sx, sy, size, size, 0, 0, size, size);
|
|
1573
1675
|
streamRef.current?.getTracks().forEach((t) => t.stop());
|
|
1574
1676
|
streamRef.current = null;
|
|
1575
|
-
onCapture(canvas.toDataURL("image/jpeg", 0.9));
|
|
1677
|
+
onCapture(canvas.toDataURL("image/jpeg", 0.9), buildLivenessArtifact(samplesRef.current));
|
|
1576
1678
|
};
|
|
1577
1679
|
if (error) {
|
|
1578
1680
|
return /* @__PURE__ */ React.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center", padding: 0 } }, /* @__PURE__ */ React.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React.createElement("h3", { style: titleStyle }, "Camera unavailable"), /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, error), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement("button", { style: secondaryButtonStyle, onClick: onCancel }, "Back")));
|
|
@@ -1580,7 +1682,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
|
|
|
1580
1682
|
const detectionActive = ready && !faceDetectionFailed;
|
|
1581
1683
|
const liveMessage = detectionActive ? FACE_MESSAGES[faceStatus] : message;
|
|
1582
1684
|
const liveFaceColor = detectionActive ? FACE_COLORS[faceStatus] : void 0;
|
|
1583
|
-
const captureDisabled =
|
|
1685
|
+
const captureDisabled = isCaptureDisabled(ready, detectionActive, faceStatus);
|
|
1584
1686
|
const ringPulse = detectionActive && (faceStatus === "too_far" || faceStatus === "too_close") ? "pulse 1.5s ease-in-out infinite" : void 0;
|
|
1585
1687
|
const dynamicCircleStyle = {
|
|
1586
1688
|
...faceCircleStyle,
|
|
@@ -2107,6 +2209,7 @@ function useEventBasedFaceVerificationSubmission(client) {
|
|
|
2107
2209
|
defaultDevice: opts.defaultDevice,
|
|
2108
2210
|
renderQR: opts.renderQR,
|
|
2109
2211
|
onCancel: opts.onCancel,
|
|
2212
|
+
mediapipeAssetBasePath: opts.mediapipeAssetBasePath,
|
|
2110
2213
|
onComplete: (result) => {
|
|
2111
2214
|
cleanup();
|
|
2112
2215
|
resolve(result);
|