vesant-sdk 1.7.0-dev.fce9898 → 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 +324 -57
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +317 -58
- 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 +90 -13
- package/dist/kyc/index.d.ts +90 -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 +73 -9
- package/dist/tax/index.js.map +1 -1
- package/dist/tax/index.mjs +73 -9
- 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.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { D as DeviceFingerprintRequest, G as GeolocationClient, i as CipherTextOptions, l as CipherTextResult, W as UseGeolocationOptions, X as UseGeolocationResult, Y as UseLocationCaptureOptions, Z as UseLocationCaptureResult, _ as UseLocationRequestsOptions, $ as UseLocationRequestsResult } from './client-
|
|
2
|
-
import { e as LoginVerificationResponse, L as LoginVerificationRequest, f as RegistrationVerificationResponse, R as RegistrationVerificationRequest, h as TransactionVerificationResponse, g as TransactionVerificationRequest, C as ComplianceClient } from './client-
|
|
1
|
+
import { D as DeviceFingerprintRequest, G as GeolocationClient, i as CipherTextOptions, l as CipherTextResult, W as UseGeolocationOptions, X as UseGeolocationResult, Y as UseLocationCaptureOptions, Z as UseLocationCaptureResult, _ as UseLocationRequestsOptions, $ as UseLocationRequestsResult } from './client-CvWNRwwg.mjs';
|
|
2
|
+
import { e as LoginVerificationResponse, L as LoginVerificationRequest, f as RegistrationVerificationResponse, R as RegistrationVerificationRequest, h as TransactionVerificationResponse, g as TransactionVerificationRequest, C as ComplianceClient } from './client-Dps40EtF.mjs';
|
|
3
3
|
import { RiskProfileClient } from './risk-profile/index.mjs';
|
|
4
4
|
import { C as CustomerProfile } from './types-BOFaMQxI.mjs';
|
|
5
5
|
import { KycClient, CreateEventBasedFaceVerificationSessionRequest, CreateEventBasedFaceVerificationSessionResponse, EventBasedFaceVerificationCallback, UseKycAlertsOptions, UseKycAlertsResult, UseKycOverviewOptions, UseKycOverviewResult, UseKycPreferencesResult, UseKycRequestsOptions, UseKycRequestsResult, UseKycSubmissionOptions, UseKycSubmissionResult } from './kyc/index.mjs';
|
|
6
|
-
import './client-
|
|
6
|
+
import './client-q9fN8Hhf.mjs';
|
|
7
7
|
import './types-CBQRNL-l.mjs';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -21,6 +21,10 @@ declare function getBrowserInfo(): {
|
|
|
21
21
|
* React Hooks for Vesant Geolocation SDK
|
|
22
22
|
*
|
|
23
23
|
* Provides convenient React hooks for using the geolocation service in Next.js applications.
|
|
24
|
+
*
|
|
25
|
+
* Scope: the web SDK collects GPS + IP only (no WiFi/cell positioning — no browser
|
|
26
|
+
* API exists). The client never asserts its own IP; the server derives it. GPS
|
|
27
|
+
* fixes are run through advisory in-browser integrity heuristics (see ./integrity).
|
|
24
28
|
*/
|
|
25
29
|
|
|
26
30
|
/**
|
|
@@ -530,11 +534,13 @@ declare function useEventBasedFaceVerificationSubmission(client: KycClient): {
|
|
|
530
534
|
defaultDevice?: "ask" | "this" | "mobile";
|
|
531
535
|
renderQR?: (payload: string) => React.ReactNode;
|
|
532
536
|
onCancel?: () => void;
|
|
537
|
+
mediapipeAssetBasePath?: string;
|
|
533
538
|
}) => Promise<EventBasedFaceVerificationCallback | null>;
|
|
534
539
|
runFlow: (request: CreateEventBasedFaceVerificationSessionRequest, opts?: {
|
|
535
540
|
defaultDevice?: "ask" | "this" | "mobile";
|
|
536
541
|
renderQR?: (payload: string) => React.ReactNode;
|
|
537
542
|
onCancel?: () => void;
|
|
543
|
+
mediapipeAssetBasePath?: string;
|
|
538
544
|
}) => Promise<{
|
|
539
545
|
kind: "not_required";
|
|
540
546
|
session: CreateEventBasedFaceVerificationSessionResponse;
|
package/dist/react.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { D as DeviceFingerprintRequest, G as GeolocationClient, i as CipherTextOptions, l as CipherTextResult, W as UseGeolocationOptions, X as UseGeolocationResult, Y as UseLocationCaptureOptions, Z as UseLocationCaptureResult, _ as UseLocationRequestsOptions, $ as UseLocationRequestsResult } from './client-
|
|
2
|
-
import { e as LoginVerificationResponse, L as LoginVerificationRequest, f as RegistrationVerificationResponse, R as RegistrationVerificationRequest, h as TransactionVerificationResponse, g as TransactionVerificationRequest, C as ComplianceClient } from './client-
|
|
1
|
+
import { D as DeviceFingerprintRequest, G as GeolocationClient, i as CipherTextOptions, l as CipherTextResult, W as UseGeolocationOptions, X as UseGeolocationResult, Y as UseLocationCaptureOptions, Z as UseLocationCaptureResult, _ as UseLocationRequestsOptions, $ as UseLocationRequestsResult } from './client-0NOPDnT0.js';
|
|
2
|
+
import { e as LoginVerificationResponse, L as LoginVerificationRequest, f as RegistrationVerificationResponse, R as RegistrationVerificationRequest, h as TransactionVerificationResponse, g as TransactionVerificationRequest, C as ComplianceClient } from './client-SMxqod4j.js';
|
|
3
3
|
import { RiskProfileClient } from './risk-profile/index.js';
|
|
4
4
|
import { C as CustomerProfile } from './types-UGyDl1fd.js';
|
|
5
5
|
import { KycClient, CreateEventBasedFaceVerificationSessionRequest, CreateEventBasedFaceVerificationSessionResponse, EventBasedFaceVerificationCallback, UseKycAlertsOptions, UseKycAlertsResult, UseKycOverviewOptions, UseKycOverviewResult, UseKycPreferencesResult, UseKycRequestsOptions, UseKycRequestsResult, UseKycSubmissionOptions, UseKycSubmissionResult } from './kyc/index.js';
|
|
6
|
-
import './client-
|
|
6
|
+
import './client-q9fN8Hhf.js';
|
|
7
7
|
import './types-CBQRNL-l.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -21,6 +21,10 @@ declare function getBrowserInfo(): {
|
|
|
21
21
|
* React Hooks for Vesant Geolocation SDK
|
|
22
22
|
*
|
|
23
23
|
* Provides convenient React hooks for using the geolocation service in Next.js applications.
|
|
24
|
+
*
|
|
25
|
+
* Scope: the web SDK collects GPS + IP only (no WiFi/cell positioning — no browser
|
|
26
|
+
* API exists). The client never asserts its own IP; the server derives it. GPS
|
|
27
|
+
* fixes are run through advisory in-browser integrity heuristics (see ./integrity).
|
|
24
28
|
*/
|
|
25
29
|
|
|
26
30
|
/**
|
|
@@ -530,11 +534,13 @@ declare function useEventBasedFaceVerificationSubmission(client: KycClient): {
|
|
|
530
534
|
defaultDevice?: "ask" | "this" | "mobile";
|
|
531
535
|
renderQR?: (payload: string) => React.ReactNode;
|
|
532
536
|
onCancel?: () => void;
|
|
537
|
+
mediapipeAssetBasePath?: string;
|
|
533
538
|
}) => Promise<EventBasedFaceVerificationCallback | null>;
|
|
534
539
|
runFlow: (request: CreateEventBasedFaceVerificationSessionRequest, opts?: {
|
|
535
540
|
defaultDevice?: "ask" | "this" | "mobile";
|
|
536
541
|
renderQR?: (payload: string) => React.ReactNode;
|
|
537
542
|
onCancel?: () => void;
|
|
543
|
+
mediapipeAssetBasePath?: string;
|
|
538
544
|
}) => Promise<{
|
|
539
545
|
kind: "not_required";
|
|
540
546
|
session: CreateEventBasedFaceVerificationSessionResponse;
|
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,6 +953,87 @@ 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;
|
|
894
1039
|
var SESSION_EXPIRY_MS = 15 * 60 * 1e3;
|
|
@@ -922,7 +1067,8 @@ function FaceCaptureModal({
|
|
|
922
1067
|
onComplete,
|
|
923
1068
|
onCancel,
|
|
924
1069
|
defaultDevice,
|
|
925
|
-
renderQR
|
|
1070
|
+
renderQR,
|
|
1071
|
+
mediapipeAssetBasePath
|
|
926
1072
|
}) {
|
|
927
1073
|
const isMobile = typeof navigator !== "undefined" && MOBILE_UA.test(navigator.userAgent);
|
|
928
1074
|
const initialChoice = defaultDevice ?? (isMobile ? "this" : "ask");
|
|
@@ -939,6 +1085,7 @@ function FaceCaptureModal({
|
|
|
939
1085
|
const [captureMode, setCaptureMode] = React.useState("idle");
|
|
940
1086
|
const [capturedPreview, setCapturedPreview] = React.useState(null);
|
|
941
1087
|
const [capturedBase64, setCapturedBase64] = React.useState(null);
|
|
1088
|
+
const [capturedArtifact, setCapturedArtifact] = React.useState(null);
|
|
942
1089
|
const [livenessMessage, setLivenessMessage] = React.useState(LIVENESS_MESSAGES[0]);
|
|
943
1090
|
const qrPayload = session.link || `vesant://reuse-kyc?token=${encodeURIComponent(session.token)}`;
|
|
944
1091
|
const cancelledRef = React.useRef(false);
|
|
@@ -1036,7 +1183,10 @@ function FaceCaptureModal({
|
|
|
1036
1183
|
const data = await client.submitEventBasedFaceVerificationSession({
|
|
1037
1184
|
token: session.token,
|
|
1038
1185
|
reference: ref,
|
|
1039
|
-
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
|
|
1040
1190
|
});
|
|
1041
1191
|
return { ok: true, data };
|
|
1042
1192
|
} catch (err2) {
|
|
@@ -1082,16 +1232,18 @@ function FaceCaptureModal({
|
|
|
1082
1232
|
submittingRef.current = false;
|
|
1083
1233
|
}
|
|
1084
1234
|
};
|
|
1085
|
-
const handleCameraCapture = (dataUrl) => {
|
|
1235
|
+
const handleCameraCapture = (dataUrl, livenessArtifact) => {
|
|
1086
1236
|
const raw = dataUrl.split(",")[1] ?? "";
|
|
1087
1237
|
setCapturedPreview(dataUrl);
|
|
1088
1238
|
setCapturedBase64(raw);
|
|
1239
|
+
setCapturedArtifact(livenessArtifact ?? null);
|
|
1089
1240
|
setCaptureMode("preview");
|
|
1090
1241
|
};
|
|
1091
1242
|
const handleCameraCancel = () => setCaptureMode("idle");
|
|
1092
1243
|
const handleRetake = () => {
|
|
1093
1244
|
setCapturedPreview(null);
|
|
1094
1245
|
setCapturedBase64(null);
|
|
1246
|
+
setCapturedArtifact(null);
|
|
1095
1247
|
setCaptureMode("live");
|
|
1096
1248
|
};
|
|
1097
1249
|
const handleConfirmSubmit = () => {
|
|
@@ -1163,7 +1315,8 @@ function FaceCaptureModal({
|
|
|
1163
1315
|
{
|
|
1164
1316
|
onCapture: handleCameraCapture,
|
|
1165
1317
|
onCancel: handleCameraCancel,
|
|
1166
|
-
message: livenessMessage
|
|
1318
|
+
message: livenessMessage,
|
|
1319
|
+
assetBasePath: mediapipeAssetBasePath
|
|
1167
1320
|
}
|
|
1168
1321
|
), /* @__PURE__ */ React__default.default.createElement("p", { style: attemptsTextStyle }, "Attempt ", attempts + 1, " of ", maxAttempts));
|
|
1169
1322
|
}
|
|
@@ -1394,59 +1547,10 @@ var attemptsTextStyle = {
|
|
|
1394
1547
|
};
|
|
1395
1548
|
var footerStyle = { padding: "12px 24px 16px", borderTop: "1px solid #f3f4f6" };
|
|
1396
1549
|
var footerTextStyle = { margin: 0, fontSize: 12, color: "#9ca3af", textAlign: "center" };
|
|
1397
|
-
|
|
1398
|
-
idle: "Initializing camera...",
|
|
1399
|
-
loading: "Loading face detection...",
|
|
1400
|
-
scanning: "Position your face in the circle",
|
|
1401
|
-
too_far: "Face too far. Please move closer to the camera",
|
|
1402
|
-
too_close: "Too close. Please move back a little",
|
|
1403
|
-
off_center: "Center your face in the circle",
|
|
1404
|
-
ok: "Looking great! Tap the button to capture"
|
|
1405
|
-
};
|
|
1406
|
-
var FACE_COLORS = {
|
|
1407
|
-
idle: "#9ca3af",
|
|
1408
|
-
loading: "#3b82f6",
|
|
1409
|
-
scanning: "#3b82f6",
|
|
1410
|
-
too_far: "#ef4444",
|
|
1411
|
-
too_close: "#f59e0b",
|
|
1412
|
-
off_center: "#f59e0b",
|
|
1413
|
-
ok: "#10b981"
|
|
1414
|
-
};
|
|
1415
|
-
var MEDIAPIPE_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/face_detection";
|
|
1416
|
-
var mediapipeLoaderPromise = null;
|
|
1417
|
-
function loadMediaPipeFaceDetection() {
|
|
1418
|
-
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
1419
|
-
return Promise.reject(new Error("MediaPipe requires a browser environment"));
|
|
1420
|
-
}
|
|
1421
|
-
const w = window;
|
|
1422
|
-
if (w.FaceDetection) {
|
|
1423
|
-
return Promise.resolve(w.FaceDetection);
|
|
1424
|
-
}
|
|
1425
|
-
if (mediapipeLoaderPromise) return mediapipeLoaderPromise;
|
|
1426
|
-
mediapipeLoaderPromise = new Promise((resolve, reject) => {
|
|
1427
|
-
const script = document.createElement("script");
|
|
1428
|
-
script.src = `${MEDIAPIPE_BASE}/face_detection.js`;
|
|
1429
|
-
script.async = true;
|
|
1430
|
-
script.crossOrigin = "anonymous";
|
|
1431
|
-
script.onload = () => {
|
|
1432
|
-
if (w.FaceDetection) {
|
|
1433
|
-
resolve(w.FaceDetection);
|
|
1434
|
-
} else {
|
|
1435
|
-
mediapipeLoaderPromise = null;
|
|
1436
|
-
reject(new Error("MediaPipe loaded but FaceDetection global is missing"));
|
|
1437
|
-
}
|
|
1438
|
-
};
|
|
1439
|
-
script.onerror = () => {
|
|
1440
|
-
mediapipeLoaderPromise = null;
|
|
1441
|
-
reject(new Error("Failed to load MediaPipe face_detection script"));
|
|
1442
|
-
};
|
|
1443
|
-
document.head.appendChild(script);
|
|
1444
|
-
});
|
|
1445
|
-
return mediapipeLoaderPromise;
|
|
1446
|
-
}
|
|
1447
|
-
function LiveCamera({ onCapture, onCancel, message }) {
|
|
1550
|
+
function LiveCamera({ onCapture, onCancel, message, assetBasePath }) {
|
|
1448
1551
|
const videoRef = React.useRef(null);
|
|
1449
1552
|
const streamRef = React.useRef(null);
|
|
1553
|
+
const samplesRef = React.useRef([]);
|
|
1450
1554
|
const [ready, setReady] = React.useState(false);
|
|
1451
1555
|
const [error, setError] = React.useState(null);
|
|
1452
1556
|
const [faceStatus, setFaceStatus] = React.useState("idle");
|
|
@@ -1501,31 +1605,29 @@ function LiveCamera({ onCapture, onCancel, message }) {
|
|
|
1501
1605
|
let detector = null;
|
|
1502
1606
|
setFaceStatus("loading");
|
|
1503
1607
|
setFaceDetectionFailed(false);
|
|
1608
|
+
samplesRef.current = [];
|
|
1504
1609
|
(async () => {
|
|
1505
1610
|
try {
|
|
1506
|
-
const FaceDetection = await loadMediaPipeFaceDetection();
|
|
1611
|
+
const FaceDetection = await loadMediaPipeFaceDetection(assetBasePath);
|
|
1507
1612
|
if (cancelled) return;
|
|
1613
|
+
const assetBase = resolveMediaPipeBase(assetBasePath);
|
|
1508
1614
|
const d = new FaceDetection({
|
|
1509
|
-
locateFile: (f) => `${
|
|
1615
|
+
locateFile: (f) => `${assetBase}/${f}`
|
|
1510
1616
|
});
|
|
1511
1617
|
d.setOptions({ model: "short", minDetectionConfidence: 0.5 });
|
|
1512
1618
|
d.onResults((results) => {
|
|
1513
1619
|
if (cancelled) return;
|
|
1514
1620
|
const detections = results.detections ?? [];
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
next = "off_center";
|
|
1524
|
-
} else {
|
|
1525
|
-
next = "ok";
|
|
1526
|
-
}
|
|
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();
|
|
1527
1629
|
}
|
|
1528
|
-
setFaceStatus(
|
|
1630
|
+
setFaceStatus(status);
|
|
1529
1631
|
});
|
|
1530
1632
|
await d.initialize();
|
|
1531
1633
|
if (cancelled) return;
|
|
@@ -1561,7 +1663,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
|
|
|
1561
1663
|
} catch {
|
|
1562
1664
|
}
|
|
1563
1665
|
};
|
|
1564
|
-
}, [ready]);
|
|
1666
|
+
}, [ready, assetBasePath]);
|
|
1565
1667
|
const capture = () => {
|
|
1566
1668
|
const video = videoRef.current;
|
|
1567
1669
|
if (!video || !video.videoWidth) return;
|
|
@@ -1578,7 +1680,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
|
|
|
1578
1680
|
ctx.drawImage(video, sx, sy, size, size, 0, 0, size, size);
|
|
1579
1681
|
streamRef.current?.getTracks().forEach((t) => t.stop());
|
|
1580
1682
|
streamRef.current = null;
|
|
1581
|
-
onCapture(canvas.toDataURL("image/jpeg", 0.9));
|
|
1683
|
+
onCapture(canvas.toDataURL("image/jpeg", 0.9), buildLivenessArtifact(samplesRef.current));
|
|
1582
1684
|
};
|
|
1583
1685
|
if (error) {
|
|
1584
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")));
|
|
@@ -1586,7 +1688,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
|
|
|
1586
1688
|
const detectionActive = ready && !faceDetectionFailed;
|
|
1587
1689
|
const liveMessage = detectionActive ? FACE_MESSAGES[faceStatus] : message;
|
|
1588
1690
|
const liveFaceColor = detectionActive ? FACE_COLORS[faceStatus] : void 0;
|
|
1589
|
-
const captureDisabled =
|
|
1691
|
+
const captureDisabled = isCaptureDisabled(ready, detectionActive, faceStatus);
|
|
1590
1692
|
const ringPulse = detectionActive && (faceStatus === "too_far" || faceStatus === "too_close") ? "pulse 1.5s ease-in-out infinite" : void 0;
|
|
1591
1693
|
const dynamicCircleStyle = {
|
|
1592
1694
|
...faceCircleStyle,
|
|
@@ -2113,6 +2215,7 @@ function useEventBasedFaceVerificationSubmission(client$1) {
|
|
|
2113
2215
|
defaultDevice: opts.defaultDevice,
|
|
2114
2216
|
renderQR: opts.renderQR,
|
|
2115
2217
|
onCancel: opts.onCancel,
|
|
2218
|
+
mediapipeAssetBasePath: opts.mediapipeAssetBasePath,
|
|
2116
2219
|
onComplete: (result) => {
|
|
2117
2220
|
cleanup();
|
|
2118
2221
|
resolve(result);
|