vesant-sdk 1.7.0-dev.362ac6b → 1.7.0-dev.3f9ec86

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.
Files changed (75) hide show
  1. package/dist/{client-DoMSYMMR.d.ts → client-0NOPDnT0.d.ts} +29 -26
  2. package/dist/{client-DMIRx7Tu.d.mts → client-CvWNRwwg.d.mts} +29 -26
  3. package/dist/{client-ZNdnpWe7.d.mts → client-Dps40EtF.d.mts} +2 -2
  4. package/dist/{client-C3DCmGe9.d.ts → client-SMxqod4j.d.ts} +2 -2
  5. package/dist/{client-BolQlL5e.d.mts → client-q9fN8Hhf.d.mts} +103 -1
  6. package/dist/{client-BolQlL5e.d.ts → client-q9fN8Hhf.d.ts} +103 -1
  7. package/dist/compliance/index.d.mts +3 -3
  8. package/dist/compliance/index.d.ts +3 -3
  9. package/dist/compliance/index.js +133 -22
  10. package/dist/compliance/index.js.map +1 -1
  11. package/dist/compliance/index.mjs +133 -22
  12. package/dist/compliance/index.mjs.map +1 -1
  13. package/dist/decisions/index.d.mts +1 -1
  14. package/dist/decisions/index.d.ts +1 -1
  15. package/dist/decisions/index.js +68 -7
  16. package/dist/decisions/index.js.map +1 -1
  17. package/dist/decisions/index.mjs +68 -7
  18. package/dist/decisions/index.mjs.map +1 -1
  19. package/dist/geolocation/index.d.mts +59 -4
  20. package/dist/geolocation/index.d.ts +59 -4
  21. package/dist/geolocation/index.js +136 -19
  22. package/dist/geolocation/index.js.map +1 -1
  23. package/dist/geolocation/index.mjs +134 -20
  24. package/dist/geolocation/index.mjs.map +1 -1
  25. package/dist/index-C3zUKydT.d.mts +264 -0
  26. package/dist/index-DSDgS09k.d.ts +264 -0
  27. package/dist/index.d.mts +27 -10
  28. package/dist/index.d.ts +27 -10
  29. package/dist/index.js +343 -69
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +336 -70
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/kyc/core.d.mts +2 -2
  34. package/dist/kyc/core.d.ts +2 -2
  35. package/dist/kyc/core.js +109 -30
  36. package/dist/kyc/core.js.map +1 -1
  37. package/dist/kyc/core.mjs +109 -30
  38. package/dist/kyc/core.mjs.map +1 -1
  39. package/dist/kyc/index.d.mts +164 -61
  40. package/dist/kyc/index.d.ts +164 -61
  41. package/dist/kyc/index.js +109 -30
  42. package/dist/kyc/index.js.map +1 -1
  43. package/dist/kyc/index.mjs +109 -30
  44. package/dist/kyc/index.mjs.map +1 -1
  45. package/dist/react.d.mts +21 -15
  46. package/dist/react.d.ts +21 -15
  47. package/dist/react.js +199 -87
  48. package/dist/react.js.map +1 -1
  49. package/dist/react.mjs +199 -87
  50. package/dist/react.mjs.map +1 -1
  51. package/dist/risk-profile/index.d.mts +1 -1
  52. package/dist/risk-profile/index.d.ts +1 -1
  53. package/dist/risk-profile/index.js +68 -7
  54. package/dist/risk-profile/index.js.map +1 -1
  55. package/dist/risk-profile/index.mjs +68 -7
  56. package/dist/risk-profile/index.mjs.map +1 -1
  57. package/dist/scores/index.d.mts +1 -1
  58. package/dist/scores/index.d.ts +1 -1
  59. package/dist/scores/index.js +68 -7
  60. package/dist/scores/index.js.map +1 -1
  61. package/dist/scores/index.mjs +68 -7
  62. package/dist/scores/index.mjs.map +1 -1
  63. package/dist/tax/index.d.mts +20 -3
  64. package/dist/tax/index.d.ts +20 -3
  65. package/dist/tax/index.js +77 -10
  66. package/dist/tax/index.js.map +1 -1
  67. package/dist/tax/index.mjs +77 -10
  68. package/dist/tax/index.mjs.map +1 -1
  69. package/dist/webhooks/index.d.mts +2 -189
  70. package/dist/webhooks/index.d.ts +2 -189
  71. package/dist/webhooks/index.js +85 -21
  72. package/dist/webhooks/index.js.map +1 -1
  73. package/dist/webhooks/index.mjs +85 -22
  74. package/dist/webhooks/index.mjs.map +1 -1
  75. package/package.json +4 -1
package/dist/react.js CHANGED
@@ -93,6 +93,61 @@ function getBrowserInfo() {
93
93
  return { browser, browser_version: browserVersion, os, os_version: osVersion };
94
94
  }
95
95
 
96
+ // src/geolocation/integrity.ts
97
+ var isNil = (v) => v === null || v === void 0;
98
+ function assessGpsIntegrity(input, opts = {}) {
99
+ const flags = [];
100
+ const now = opts.now ?? (() => Date.now());
101
+ const maxAgeMs = opts.maxAgeMs ?? 6e4;
102
+ const minAccuracy = opts.minAccuracyMeters ?? 1;
103
+ const { accuracy, altitude, altitudeAccuracy, heading, speed, timestamp } = input;
104
+ if (isNil(accuracy)) {
105
+ flags.push("accuracy_missing");
106
+ } else if (accuracy === 0) {
107
+ flags.push("accuracy_zero");
108
+ } else if (accuracy < minAccuracy) {
109
+ flags.push("accuracy_implausibly_small");
110
+ }
111
+ const hasMotion = !isNil(heading) || !isNil(speed);
112
+ if (hasMotion && (isNil(accuracy) || accuracy === 0)) {
113
+ flags.push("motion_without_accuracy");
114
+ }
115
+ if (!isNil(altitude) && isNil(altitudeAccuracy)) {
116
+ flags.push("altitude_without_accuracy");
117
+ }
118
+ if (timestamp !== void 0 && now() - timestamp > maxAgeMs) {
119
+ flags.push("stale_timestamp");
120
+ }
121
+ if (opts.permissionState === "denied" || opts.permissionState === "prompt") {
122
+ flags.push(`permission_${opts.permissionState}`);
123
+ }
124
+ return { suspicious: flags.length > 0, flags };
125
+ }
126
+ function gpsIntegrityInputFromPosition(position) {
127
+ const c = position.coords;
128
+ return {
129
+ latitude: c.latitude,
130
+ longitude: c.longitude,
131
+ accuracy: c.accuracy,
132
+ altitude: c.altitude,
133
+ altitudeAccuracy: c.altitudeAccuracy,
134
+ heading: c.heading,
135
+ speed: c.speed,
136
+ timestamp: position.timestamp
137
+ };
138
+ }
139
+ async function probeGeolocationPermission() {
140
+ try {
141
+ if (typeof navigator === "undefined" || !navigator.permissions?.query) {
142
+ return "unavailable";
143
+ }
144
+ const status = await navigator.permissions.query({ name: "geolocation" });
145
+ return status.state;
146
+ } catch {
147
+ return "unavailable";
148
+ }
149
+ }
150
+
96
151
  // src/geolocation/ciphertext.ts
97
152
  var CIPHER_TEXT_EXPIRY_MINUTES = 5;
98
153
  async function computeHMAC(key, message) {
@@ -145,6 +200,7 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
145
200
  if (typeof navigator === "undefined" || !navigator.geolocation) {
146
201
  return null;
147
202
  }
203
+ const permissionState = await probeGeolocationPermission();
148
204
  return new Promise((resolve) => {
149
205
  navigator.geolocation.getCurrentPosition(
150
206
  (position) => {
@@ -161,6 +217,7 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
161
217
  resolve(null);
162
218
  return;
163
219
  }
220
+ const { flags } = assessGpsIntegrity(gpsIntegrityInputFromPosition(position), { permissionState });
164
221
  resolve({
165
222
  latitude,
166
223
  longitude,
@@ -169,7 +226,8 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
169
226
  altitude_accuracy: position.coords.altitudeAccuracy ?? void 0,
170
227
  heading: position.coords.heading ?? void 0,
171
228
  speed: position.coords.speed ?? void 0,
172
- timestamp: position.timestamp
229
+ timestamp: position.timestamp,
230
+ integrity_flags: flags.length ? flags : void 0
173
231
  });
174
232
  },
175
233
  () => {
@@ -236,6 +294,9 @@ async function generateCipherText(options, config) {
236
294
  );
237
295
  if (location) {
238
296
  locationData = location;
297
+ if (location.integrity_flags?.length) {
298
+ warnings.push(`GPS integrity flags (advisory): ${location.integrity_flags.join(", ")}`);
299
+ }
239
300
  } else if (gpsRequiredByConfig) {
240
301
  throw new VesantError(
241
302
  `GPS location is required for ${options.reason} by tenant configuration, but GPS was not available or permission was denied`,
@@ -338,8 +399,6 @@ function useGeolocation(client, options = {}) {
338
399
  if (optionsRef.current.autoVerify) {
339
400
  const detectAndVerify = async () => {
340
401
  try {
341
- const ipResponse = await fetch("https://api.ipify.org?format=json");
342
- const { ip } = await ipResponse.json();
343
402
  let deviceFingerprint;
344
403
  if (optionsRef.current.includeDeviceFingerprint && typeof window !== "undefined") {
345
404
  const deviceId = generateDeviceId();
@@ -353,7 +412,6 @@ function useGeolocation(client, options = {}) {
353
412
  };
354
413
  }
355
414
  await verifyIP({
356
- ip_address: ip,
357
415
  user_id: optionsRef.current.userId || "",
358
416
  event_type: optionsRef.current.eventType || "page_view",
359
417
  device_fingerprint: deviceFingerprint
@@ -667,6 +725,12 @@ function useLocationCapture(client, options) {
667
725
  if (accuracy !== null && accuracy !== void 0 && accuracy > 0) {
668
726
  capture.accuracy = accuracy;
669
727
  }
728
+ const { flags } = assessGpsIntegrity(gpsIntegrityInputFromPosition(position), {
729
+ permissionState: await probeGeolocationPermission()
730
+ });
731
+ if (flags.length) {
732
+ capture.integrity_flags = flags;
733
+ }
670
734
  }
671
735
  }
672
736
  if (gpsError) {
@@ -889,8 +953,90 @@ var Camera = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0i
889
953
  var Done = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik04LjUgMTIuNUwxMC41IDE0LjVMMTUuNSA5LjUiIHN0cm9rZT0iIzFDMjc0QyIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPg0KPHBhdGggZD0iTTcgMy4zMzc4MkM4LjQ3MDg3IDIuNDg2OTcgMTAuMTc4NiAyIDEyIDJDMTcuNTIyOCAyIDIyIDYuNDc3MTUgMjIgMTJDMjIgMTcuNTIyOCAxNy41MjI4IDIyIDEyIDIyQzYuNDc3MTUgMjIgMiAxNy41MjI4IDIgMTJDMiAxMC4xNzg2IDIuNDg2OTcgOC40NzA4NyAzLjMzNzgyIDciIHN0cm9rZT0iIzFDMjc0QyIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPg0KPC9zdmc+";
890
954
  var Close = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik0xNC41IDkuNTAwMDJMOS41IDE0LjVNOS40OTk5OCA5LjVMMTQuNSAxNC41IiBzdHJva2U9IiMxQzI3NEMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4NCjxwYXRoIGQ9Ik03IDMuMzM3ODJDOC40NzA4NyAyLjQ4Njk3IDEwLjE3ODYgMiAxMiAyQzE3LjUyMjggMiAyMiA2LjQ3NzE1IDIyIDEyQzIyIDE3LjUyMjggMTcuNTIyOCAyMiAxMiAyMkM2LjQ3NzE1IDIyIDIgMTcuNTIyOCAyIDEyQzIgMTAuMTc4NiAyLjQ4Njk3IDguNDcwODcgMy4zMzc4MiA3IiBzdHJva2U9IiMxQzI3NEMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4NCjwvc3ZnPg==";
891
955
 
956
+ // src/kyc/faceDetection.ts
957
+ var FACE_MESSAGES = {
958
+ idle: "Initializing camera...",
959
+ loading: "Loading face detection...",
960
+ scanning: "Position your face in the circle",
961
+ too_far: "Face too far. Please move closer to the camera",
962
+ too_close: "Too close. Please move back a little",
963
+ off_center: "Center your face in the circle",
964
+ ok: "Looking great! Tap the button to capture"
965
+ };
966
+ var FACE_COLORS = {
967
+ idle: "#9ca3af",
968
+ loading: "#3b82f6",
969
+ scanning: "#3b82f6",
970
+ too_far: "#ef4444",
971
+ too_close: "#f59e0b",
972
+ off_center: "#f59e0b",
973
+ ok: "#10b981"
974
+ };
975
+ var MEDIAPIPE_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/face_detection";
976
+ function mapDetectionsToFaceStatus(detections) {
977
+ if (detections.length === 0) return "scanning";
978
+ const bb = detections[0].boundingBox;
979
+ if (bb.width < 0.18) return "too_far";
980
+ if (bb.width > 0.72) return "too_close";
981
+ if (bb.xCenter < 0.28 || bb.xCenter > 0.72 || bb.yCenter < 0.28 || bb.yCenter > 0.72) {
982
+ return "off_center";
983
+ }
984
+ return "ok";
985
+ }
986
+ function isCaptureDisabled(ready, detectionActive, faceStatus) {
987
+ return !ready || detectionActive && faceStatus !== "ok";
988
+ }
989
+ function resolveMediaPipeBase(base) {
990
+ if (!base) return MEDIAPIPE_BASE;
991
+ return base.endsWith("/") ? base.slice(0, -1) : base;
992
+ }
993
+ var LIVENESS_SAMPLE_LIMIT = 20;
994
+ function buildLivenessArtifact(samples) {
995
+ if (samples.length === 0) return void 0;
996
+ return JSON.stringify({
997
+ version: 1,
998
+ source: "mediapipe-face-detection",
999
+ samples: samples.slice(-LIVENESS_SAMPLE_LIMIT)
1000
+ });
1001
+ }
1002
+ var mediapipeLoaderPromises = /* @__PURE__ */ new Map();
1003
+ function loadMediaPipeFaceDetection(assetBasePath) {
1004
+ if (typeof window === "undefined" || typeof document === "undefined") {
1005
+ return Promise.reject(new Error("MediaPipe requires a browser environment"));
1006
+ }
1007
+ const w = window;
1008
+ if (w.FaceDetection) {
1009
+ return Promise.resolve(w.FaceDetection);
1010
+ }
1011
+ const src = `${resolveMediaPipeBase(assetBasePath)}/face_detection.js`;
1012
+ const existing = mediapipeLoaderPromises.get(src);
1013
+ if (existing) return existing;
1014
+ const loader = new Promise((resolve, reject) => {
1015
+ const script = document.createElement("script");
1016
+ script.src = src;
1017
+ script.async = true;
1018
+ script.crossOrigin = "anonymous";
1019
+ script.onload = () => {
1020
+ if (w.FaceDetection) {
1021
+ resolve(w.FaceDetection);
1022
+ } else {
1023
+ mediapipeLoaderPromises.delete(src);
1024
+ reject(new Error("MediaPipe loaded but FaceDetection global is missing"));
1025
+ }
1026
+ };
1027
+ script.onerror = () => {
1028
+ mediapipeLoaderPromises.delete(src);
1029
+ reject(new Error("Failed to load MediaPipe face_detection script"));
1030
+ };
1031
+ document.head.appendChild(script);
1032
+ });
1033
+ mediapipeLoaderPromises.set(src, loader);
1034
+ return loader;
1035
+ }
1036
+
892
1037
  // src/kyc/FaceCaptureModal.tsx
893
1038
  var MOBILE_UA = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
1039
+ var SESSION_EXPIRY_MS = 15 * 60 * 1e3;
894
1040
  var LIVENESS_MESSAGES = [
895
1041
  "Position your face inside the circle",
896
1042
  "Make sure your face is well lit",
@@ -921,7 +1067,8 @@ function FaceCaptureModal({
921
1067
  onComplete,
922
1068
  onCancel,
923
1069
  defaultDevice,
924
- renderQR
1070
+ renderQR,
1071
+ mediapipeAssetBasePath
925
1072
  }) {
926
1073
  const isMobile = typeof navigator !== "undefined" && MOBILE_UA.test(navigator.userAgent);
927
1074
  const initialChoice = defaultDevice ?? (isMobile ? "this" : "ask");
@@ -938,21 +1085,30 @@ function FaceCaptureModal({
938
1085
  const [captureMode, setCaptureMode] = React.useState("idle");
939
1086
  const [capturedPreview, setCapturedPreview] = React.useState(null);
940
1087
  const [capturedBase64, setCapturedBase64] = React.useState(null);
1088
+ const [capturedArtifact, setCapturedArtifact] = React.useState(null);
941
1089
  const [livenessMessage, setLivenessMessage] = React.useState(LIVENESS_MESSAGES[0]);
942
1090
  const qrPayload = session.link || `vesant://reuse-kyc?token=${encodeURIComponent(session.token)}`;
943
1091
  const cancelledRef = React.useRef(false);
944
1092
  const submittingRef = React.useRef(false);
1093
+ React.useEffect(() => {
1094
+ const timer = setTimeout(() => {
1095
+ setStage(
1096
+ (prev) => prev.kind === "choose" || prev.kind === "qr" || prev.kind === "capture" ? { kind: "error", message: "Session expired" } : prev
1097
+ );
1098
+ }, SESSION_EXPIRY_MS);
1099
+ return () => clearTimeout(timer);
1100
+ }, [session.token]);
945
1101
  React.useEffect(() => {
946
1102
  if (stage.kind !== "qr") return;
947
1103
  let stopped = false;
948
1104
  const intervalMs = 2e3;
949
- const deadline = Date.now() + 15 * 60 * 1e3;
1105
+ const deadline = Date.now() + SESSION_EXPIRY_MS;
950
1106
  let mobileSeen = stage.mobileConnected;
951
1107
  const tick = async () => {
952
1108
  if (stopped || cancelledRef.current) return;
953
1109
  try {
954
1110
  if (mobileSeen) {
955
- const result = await client.getReuseKycSessionStatus(session.reference);
1111
+ const result = await client.getEventBasedFaceVerificationSessionStatus(session.token);
956
1112
  if (result.status === "accepted") {
957
1113
  setStage({ kind: "accepted", result });
958
1114
  stopped = true;
@@ -1024,10 +1180,13 @@ function FaceCaptureModal({
1024
1180
  try {
1025
1181
  const postPromise = (async () => {
1026
1182
  try {
1027
- const data = await client.submitReuseKycSession({
1183
+ const data = await client.submitEventBasedFaceVerificationSession({
1028
1184
  token: session.token,
1029
1185
  reference: ref,
1030
- proof
1186
+ proof,
1187
+ // undefined when detection wasn't running for this capture —
1188
+ // JSON.stringify drops it, so the field is absent on the wire.
1189
+ liveness_artifact: capturedArtifact ?? void 0
1031
1190
  });
1032
1191
  return { ok: true, data };
1033
1192
  } catch (err2) {
@@ -1040,7 +1199,7 @@ function FaceCaptureModal({
1040
1199
  while (Date.now() < deadline) {
1041
1200
  if (settled || cancelledRef.current) return null;
1042
1201
  try {
1043
- const r = await client.getReuseKycSessionStatus(ref);
1202
+ const r = await client.getEventBasedFaceVerificationSessionStatus(session.token);
1044
1203
  if (isFreshPollResult(r)) return r;
1045
1204
  } catch {
1046
1205
  }
@@ -1073,16 +1232,18 @@ function FaceCaptureModal({
1073
1232
  submittingRef.current = false;
1074
1233
  }
1075
1234
  };
1076
- const handleCameraCapture = (dataUrl) => {
1235
+ const handleCameraCapture = (dataUrl, livenessArtifact) => {
1077
1236
  const raw = dataUrl.split(",")[1] ?? "";
1078
1237
  setCapturedPreview(dataUrl);
1079
1238
  setCapturedBase64(raw);
1239
+ setCapturedArtifact(livenessArtifact ?? null);
1080
1240
  setCaptureMode("preview");
1081
1241
  };
1082
1242
  const handleCameraCancel = () => setCaptureMode("idle");
1083
1243
  const handleRetake = () => {
1084
1244
  setCapturedPreview(null);
1085
1245
  setCapturedBase64(null);
1246
+ setCapturedArtifact(null);
1086
1247
  setCaptureMode("live");
1087
1248
  };
1088
1249
  const handleConfirmSubmit = () => {
@@ -1154,14 +1315,15 @@ function FaceCaptureModal({
1154
1315
  {
1155
1316
  onCapture: handleCameraCapture,
1156
1317
  onCancel: handleCameraCancel,
1157
- message: livenessMessage
1318
+ message: livenessMessage,
1319
+ assetBasePath: mediapipeAssetBasePath
1158
1320
  }
1159
1321
  ), /* @__PURE__ */ React__default.default.createElement("p", { style: attemptsTextStyle }, "Attempt ", attempts + 1, " of ", maxAttempts));
1160
1322
  }
1161
1323
  if (captureMode === "preview" && capturedPreview) {
1162
1324
  return /* @__PURE__ */ React__default.default.createElement("div", { style: contentStyle }, declinedReason && /* @__PURE__ */ React__default.default.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React__default.default.createElement("strong", null, "Verification declined."), /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, declinedReason)), /* @__PURE__ */ React__default.default.createElement("p", { style: subtitleStyle }, "Looks good? Submit this selfie or retake it."), /* @__PURE__ */ React__default.default.createElement("div", { style: previewBoxStyle }, /* @__PURE__ */ React__default.default.createElement("img", { src: capturedPreview, alt: "Captured selfie preview", style: previewImgStyle })), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: handleConfirmSubmit }, /* @__PURE__ */ React__default.default.createElement("img", { src: Done, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React__default.default.createElement("span", null, "Submit")), /* @__PURE__ */ React__default.default.createElement("button", { style: secondaryButtonStyle, onClick: handleRetake }, "Retake"), /* @__PURE__ */ React__default.default.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, "Cancel")), /* @__PURE__ */ React__default.default.createElement("p", { style: attemptsTextStyle }, "Attempt ", attempts + 1, " of ", maxAttempts));
1163
1325
  }
1164
- return /* @__PURE__ */ React__default.default.createElement("div", { style: contentStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: visualGuideContainerStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: visualGuideStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: circleStyle }, /* @__PURE__ */ React__default.default.createElement("img", { src: Camera, alt: "", width: 48, height: 48 })), /* @__PURE__ */ React__default.default.createElement("div", { style: badgeStyle }, /* @__PURE__ */ React__default.default.createElement("img", { src: Done, alt: "", width: 16, height: 16 })))), declinedReason && /* @__PURE__ */ React__default.default.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React__default.default.createElement("strong", null, "Verification declined."), /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, declinedReason)), /* @__PURE__ */ React__default.default.createElement("div", { style: instructionsBoxStyle }, /* @__PURE__ */ React__default.default.createElement("h3", { style: instructionsTitleStyle }, "Tips for best results:"), /* @__PURE__ */ React__default.default.createElement("ul", { style: instructionsListStyle }, /* @__PURE__ */ React__default.default.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React__default.default.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React__default.default.createElement("span", null, "Ensure good lighting on your face")), /* @__PURE__ */ React__default.default.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React__default.default.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React__default.default.createElement("span", null, "Remove glasses or accessories if possible")), /* @__PURE__ */ React__default.default.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React__default.default.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React__default.default.createElement("span", null, "Look directly at the camera")))), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: handleOpenCamera }, /* @__PURE__ */ React__default.default.createElement("img", { src: Camera, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React__default.default.createElement("span", null, declinedReason ? "Try again" : "Open Camera")), hasMethodChoice && /* @__PURE__ */ React__default.default.createElement(
1326
+ return /* @__PURE__ */ React__default.default.createElement("div", { style: contentStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: visualGuideContainerStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: visualGuideStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: circleStyle }, /* @__PURE__ */ React__default.default.createElement("img", { src: Camera, alt: "", width: 48, height: 48 })), /* @__PURE__ */ React__default.default.createElement("div", { style: badgeStyle }, /* @__PURE__ */ React__default.default.createElement("img", { src: Done, alt: "", width: 16, height: 16 })))), declinedReason && /* @__PURE__ */ React__default.default.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React__default.default.createElement("strong", null, "Verification declined."), /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, declinedReason)), /* @__PURE__ */ React__default.default.createElement("div", { style: instructionsBoxStyle }, /* @__PURE__ */ React__default.default.createElement("h3", { style: instructionsTitleStyle }, "Tips for best results:"), /* @__PURE__ */ React__default.default.createElement("ul", { style: instructionsListStyle }, /* @__PURE__ */ React__default.default.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React__default.default.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React__default.default.createElement("span", null, "Ensure good lighting on your face")), /* @__PURE__ */ React__default.default.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React__default.default.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React__default.default.createElement("span", null, "Remove glasses or accessories if possible")), /* @__PURE__ */ React__default.default.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React__default.default.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React__default.default.createElement("span", null, "Look directly at the camera")))), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: handleOpenCamera }, /* @__PURE__ */ React__default.default.createElement("img", { src: Camera, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React__default.default.createElement("span", null, declinedReason ? "Try Again" : "Open Camera")), hasMethodChoice && /* @__PURE__ */ React__default.default.createElement(
1165
1327
  "button",
1166
1328
  {
1167
1329
  style: secondaryButtonStyle,
@@ -1173,10 +1335,10 @@ function FaceCaptureModal({
1173
1335
  const renderSubmitting = () => /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: { ...spinnerStyle, margin: "24px auto", width: 32, height: 32 } }), /* @__PURE__ */ React__default.default.createElement("p", { style: subtitleStyle }, "Verifying your photo\u2026"), /* @__PURE__ */ React__default.default.createElement("p", { style: { ...attemptsTextStyle, marginTop: 8 } }, "This usually takes a few seconds."));
1174
1336
  const renderAccepted = (result) => /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: successCircleStyle }, /* @__PURE__ */ React__default.default.createElement("img", { src: Done, alt: "", width: 48, height: 48 })), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, "Verified"), /* @__PURE__ */ React__default.default.createElement("p", { style: subtitleStyle }, "Your identity has been confirmed. You can continue."), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: () => close(result) }, "Continue")));
1175
1337
  const renderDeclined = (result) => /* @__PURE__ */ React__default.default.createElement("div", { style: contentStyle }, /* @__PURE__ */ React__default.default.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, result.declined_reason ?? "We couldn't match your selfie. Please take a new one.")), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: handleRetakeAfterDecline }, /* @__PURE__ */ React__default.default.createElement("img", { src: Camera, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React__default.default.createElement("span", null, "Retake")), /* @__PURE__ */ React__default.default.createElement("button", { style: cancelButtonStyle, onClick: () => close(result) }, "Cancel")), /* @__PURE__ */ React__default.default.createElement("p", { style: attemptsTextStyle }, "Attempt ", Math.min(attempts + 1, maxAttempts), " of ", maxAttempts));
1176
- const renderMaxAttempts = (result) => /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, "Maximum Attempts Reached"), /* @__PURE__ */ React__default.default.createElement("p", { style: subtitleStyle }, result.declined_reason ?? "We couldn't verify your identity after several attempts."), result.data?.freeze_account && /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, "For your security, your account has been temporarily restricted", result.data.freeze_duration_minutes ? ` for ${result.data.freeze_duration_minutes} minutes` : "", ". Please contact support if you need immediate help."), result.data?.enforce_logout && /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, "You will be signed out of your session."), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: () => close(result) }, "Close")));
1338
+ const renderMaxAttempts = (result) => /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, "Maximum Attempts Reached"), /* @__PURE__ */ React__default.default.createElement("p", { style: subtitleStyle }, result.declined_reason ?? "We couldn't verify your identity after several attempts."), result.data?.freeze_account && /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, "For your security, your account has been temporarily restricted", result.data.freeze_duration_minutes ? ` for ${result.data.freeze_duration_minutes} minutes` : "", ". Please contact support if you need immediate help."), result.data?.enforce_logout && /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, "You will be signed out of your session."), result.data?.block && /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, "This action has been blocked for security reasons."), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: () => close(result) }, "Close")));
1177
1339
  const renderError = (message) => {
1178
1340
  const sessionExpired = /session\s+expired/i.test(message);
1179
- return /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, sessionExpired ? "Session expired" : "Something went wrong"), /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, sessionExpired ? "Your verification session is no longer valid. Please start a new verification from the beginning." : message), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, !sessionExpired && /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: () => setStage({ kind: "capture" }) }, "Try again"), /* @__PURE__ */ React__default.default.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, sessionExpired ? "Close" : "Cancel")));
1341
+ return /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React__default.default.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, sessionExpired ? "Session expired" : "Something Went Wrong"), /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, sessionExpired ? "Your verification session is no longer valid. Please start a new verification from the beginning." : message), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, !sessionExpired && /* @__PURE__ */ React__default.default.createElement("button", { style: primaryButtonStyle, onClick: () => setStage({ kind: "capture" }) }, "Try Again"), /* @__PURE__ */ React__default.default.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, sessionExpired ? "Close" : "Cancel")));
1180
1342
  };
1181
1343
  const body = (() => {
1182
1344
  switch (stage.kind) {
@@ -1385,59 +1547,10 @@ var attemptsTextStyle = {
1385
1547
  };
1386
1548
  var footerStyle = { padding: "12px 24px 16px", borderTop: "1px solid #f3f4f6" };
1387
1549
  var footerTextStyle = { margin: 0, fontSize: 12, color: "#9ca3af", textAlign: "center" };
1388
- var FACE_MESSAGES = {
1389
- idle: "Initializing camera...",
1390
- loading: "Loading face detection...",
1391
- scanning: "Position your face in the circle",
1392
- too_far: "Face too far. Please move closer to the camera",
1393
- too_close: "Too close. Please move back a little",
1394
- off_center: "Center your face in the circle",
1395
- ok: "Looking great! Tap the button to capture"
1396
- };
1397
- var FACE_COLORS = {
1398
- idle: "#9ca3af",
1399
- loading: "#3b82f6",
1400
- scanning: "#3b82f6",
1401
- too_far: "#ef4444",
1402
- too_close: "#f59e0b",
1403
- off_center: "#f59e0b",
1404
- ok: "#10b981"
1405
- };
1406
- var MEDIAPIPE_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/face_detection";
1407
- var mediapipeLoaderPromise = null;
1408
- function loadMediaPipeFaceDetection() {
1409
- if (typeof window === "undefined" || typeof document === "undefined") {
1410
- return Promise.reject(new Error("MediaPipe requires a browser environment"));
1411
- }
1412
- const w = window;
1413
- if (w.FaceDetection) {
1414
- return Promise.resolve(w.FaceDetection);
1415
- }
1416
- if (mediapipeLoaderPromise) return mediapipeLoaderPromise;
1417
- mediapipeLoaderPromise = new Promise((resolve, reject) => {
1418
- const script = document.createElement("script");
1419
- script.src = `${MEDIAPIPE_BASE}/face_detection.js`;
1420
- script.async = true;
1421
- script.crossOrigin = "anonymous";
1422
- script.onload = () => {
1423
- if (w.FaceDetection) {
1424
- resolve(w.FaceDetection);
1425
- } else {
1426
- mediapipeLoaderPromise = null;
1427
- reject(new Error("MediaPipe loaded but FaceDetection global is missing"));
1428
- }
1429
- };
1430
- script.onerror = () => {
1431
- mediapipeLoaderPromise = null;
1432
- reject(new Error("Failed to load MediaPipe face_detection script"));
1433
- };
1434
- document.head.appendChild(script);
1435
- });
1436
- return mediapipeLoaderPromise;
1437
- }
1438
- function LiveCamera({ onCapture, onCancel, message }) {
1550
+ function LiveCamera({ onCapture, onCancel, message, assetBasePath }) {
1439
1551
  const videoRef = React.useRef(null);
1440
1552
  const streamRef = React.useRef(null);
1553
+ const samplesRef = React.useRef([]);
1441
1554
  const [ready, setReady] = React.useState(false);
1442
1555
  const [error, setError] = React.useState(null);
1443
1556
  const [faceStatus, setFaceStatus] = React.useState("idle");
@@ -1492,31 +1605,29 @@ function LiveCamera({ onCapture, onCancel, message }) {
1492
1605
  let detector = null;
1493
1606
  setFaceStatus("loading");
1494
1607
  setFaceDetectionFailed(false);
1608
+ samplesRef.current = [];
1495
1609
  (async () => {
1496
1610
  try {
1497
- const FaceDetection = await loadMediaPipeFaceDetection();
1611
+ const FaceDetection = await loadMediaPipeFaceDetection(assetBasePath);
1498
1612
  if (cancelled) return;
1613
+ const assetBase = resolveMediaPipeBase(assetBasePath);
1499
1614
  const d = new FaceDetection({
1500
- locateFile: (f) => `${MEDIAPIPE_BASE}/${f}`
1615
+ locateFile: (f) => `${assetBase}/${f}`
1501
1616
  });
1502
1617
  d.setOptions({ model: "short", minDetectionConfidence: 0.5 });
1503
1618
  d.onResults((results) => {
1504
1619
  if (cancelled) return;
1505
1620
  const detections = results.detections ?? [];
1506
- let next;
1507
- if (detections.length === 0) {
1508
- next = "scanning";
1509
- } else {
1510
- const bb = detections[0].boundingBox;
1511
- if (bb.width < 0.18) next = "too_far";
1512
- else if (bb.width > 0.72) next = "too_close";
1513
- else if (bb.xCenter < 0.28 || bb.xCenter > 0.72 || bb.yCenter < 0.28 || bb.yCenter > 0.72) {
1514
- next = "off_center";
1515
- } else {
1516
- next = "ok";
1517
- }
1621
+ const status = mapDetectionsToFaceStatus(detections);
1622
+ samplesRef.current.push({
1623
+ t: Date.now(),
1624
+ status,
1625
+ ...detections.length > 0 ? { boundingBox: detections[0].boundingBox } : {}
1626
+ });
1627
+ if (samplesRef.current.length > LIVENESS_SAMPLE_LIMIT) {
1628
+ samplesRef.current.shift();
1518
1629
  }
1519
- setFaceStatus(next);
1630
+ setFaceStatus(status);
1520
1631
  });
1521
1632
  await d.initialize();
1522
1633
  if (cancelled) return;
@@ -1552,7 +1663,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
1552
1663
  } catch {
1553
1664
  }
1554
1665
  };
1555
- }, [ready]);
1666
+ }, [ready, assetBasePath]);
1556
1667
  const capture = () => {
1557
1668
  const video = videoRef.current;
1558
1669
  if (!video || !video.videoWidth) return;
@@ -1569,7 +1680,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
1569
1680
  ctx.drawImage(video, sx, sy, size, size, 0, 0, size, size);
1570
1681
  streamRef.current?.getTracks().forEach((t) => t.stop());
1571
1682
  streamRef.current = null;
1572
- onCapture(canvas.toDataURL("image/jpeg", 0.9));
1683
+ onCapture(canvas.toDataURL("image/jpeg", 0.9), buildLivenessArtifact(samplesRef.current));
1573
1684
  };
1574
1685
  if (error) {
1575
1686
  return /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center", padding: 0 } }, /* @__PURE__ */ React__default.default.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, "Camera unavailable"), /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, error), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: secondaryButtonStyle, onClick: onCancel }, "Back")));
@@ -1577,7 +1688,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
1577
1688
  const detectionActive = ready && !faceDetectionFailed;
1578
1689
  const liveMessage = detectionActive ? FACE_MESSAGES[faceStatus] : message;
1579
1690
  const liveFaceColor = detectionActive ? FACE_COLORS[faceStatus] : void 0;
1580
- const captureDisabled = !ready || detectionActive && faceStatus !== "ok";
1691
+ const captureDisabled = isCaptureDisabled(ready, detectionActive, faceStatus);
1581
1692
  const ringPulse = detectionActive && (faceStatus === "too_far" || faceStatus === "too_close") ? "pulse 1.5s ease-in-out infinite" : void 0;
1582
1693
  const dynamicCircleStyle = {
1583
1694
  ...faceCircleStyle,
@@ -2084,9 +2195,9 @@ function getRiskColor(risk) {
2084
2195
  };
2085
2196
  return colorMap[risk] || "#6b7280";
2086
2197
  }
2087
- function useReuseKYCSubmission(client$1) {
2198
+ function useEventBasedFaceVerificationSubmission(client$1) {
2088
2199
  const startSession = React.useCallback(
2089
- (request) => client$1.createReuseKycSession(request),
2200
+ (request) => client$1.createEventBasedFaceVerificationSession(request),
2090
2201
  [client$1]
2091
2202
  );
2092
2203
  const verifyFace = React.useCallback(
@@ -2104,6 +2215,7 @@ function useReuseKYCSubmission(client$1) {
2104
2215
  defaultDevice: opts.defaultDevice,
2105
2216
  renderQR: opts.renderQR,
2106
2217
  onCancel: opts.onCancel,
2218
+ mediapipeAssetBasePath: opts.mediapipeAssetBasePath,
2107
2219
  onComplete: (result) => {
2108
2220
  cleanup();
2109
2221
  resolve(result);
@@ -2140,6 +2252,7 @@ exports.isValidFileSize = isValidFileSize;
2140
2252
  exports.isValidFileType = isValidFileType;
2141
2253
  exports.useCipherText = useCipherText;
2142
2254
  exports.useCustomerProfile = useCustomerProfile;
2255
+ exports.useEventBasedFaceVerificationSubmission = useEventBasedFaceVerificationSubmission;
2143
2256
  exports.useGeolocation = useGeolocation;
2144
2257
  exports.useKycAlerts = useKycAlerts;
2145
2258
  exports.useKycOverview = useKycOverview;
@@ -2150,7 +2263,6 @@ exports.useLocationCapture = useLocationCapture;
2150
2263
  exports.useLocationRequests = useLocationRequests;
2151
2264
  exports.useLoginVerification = useLoginVerification;
2152
2265
  exports.useRegistration = useRegistration;
2153
- exports.useReuseKYCSubmission = useReuseKYCSubmission;
2154
2266
  exports.useTransactionVerification = useTransactionVerification;
2155
2267
  //# sourceMappingURL=react.js.map
2156
2268
  //# sourceMappingURL=react.js.map