@neofaceid/web-sdk 1.4.2 → 1.6.1
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.
|
@@ -1118,6 +1118,7 @@ var ErrorType = /* @__PURE__ */ ((ErrorType2) => {
|
|
|
1118
1118
|
ErrorType2["PERSON_NOT_FOUND"] = "PersonNotFoundError";
|
|
1119
1119
|
ErrorType2["INITIALIZATION_ERROR"] = "InitializationError";
|
|
1120
1120
|
ErrorType2["CAMERA_ERROR"] = "CameraError";
|
|
1121
|
+
ErrorType2["NO_CAMERA"] = "NoCameraError";
|
|
1121
1122
|
ErrorType2["CAPTURE_ERROR"] = "CaptureError";
|
|
1122
1123
|
ErrorType2["NOT_FOUND"] = "NotFoundError";
|
|
1123
1124
|
ErrorType2["UNKNOWN"] = "UnknownError";
|
|
@@ -1528,7 +1529,7 @@ const recognizeBiometric = async (image, applicationToken, livenessCheck = true,
|
|
|
1528
1529
|
const data = await response.json();
|
|
1529
1530
|
return {
|
|
1530
1531
|
success: data.success,
|
|
1531
|
-
personId
|
|
1532
|
+
// personId is intentionally NOT returned for security reasons
|
|
1532
1533
|
confidenceScore: data.confidence_score,
|
|
1533
1534
|
accessToken: data.access_token,
|
|
1534
1535
|
payload: data.payload
|
|
@@ -1592,7 +1593,8 @@ const simpleIdentification = async (documentType, documentNumber, applicationTok
|
|
|
1592
1593
|
throw new NeoFaceError("Unknown error occurred", ErrorType.NETWORK_ERROR);
|
|
1593
1594
|
}
|
|
1594
1595
|
};
|
|
1595
|
-
const recognizeByPurpose = async (image, applicationToken, purpose, confidenceThreshold = 0.8) => {
|
|
1596
|
+
const recognizeByPurpose = async (image, applicationToken, purpose, confidenceThreshold = 0.8, signature, sessionData) => {
|
|
1597
|
+
var _a2;
|
|
1596
1598
|
ensureSecureContext();
|
|
1597
1599
|
const controller = createTimeoutController();
|
|
1598
1600
|
const compressedImage = await compressImage(image);
|
|
@@ -1606,18 +1608,22 @@ const recognizeByPurpose = async (image, applicationToken, purpose, confidenceTh
|
|
|
1606
1608
|
reader.readAsDataURL(compressedImage);
|
|
1607
1609
|
});
|
|
1608
1610
|
try {
|
|
1611
|
+
const requestBody = {
|
|
1612
|
+
biometric_data: base64Image,
|
|
1613
|
+
type_of_identification: "FACE",
|
|
1614
|
+
purpose,
|
|
1615
|
+
confidence_threshold: confidenceThreshold
|
|
1616
|
+
};
|
|
1617
|
+
if (signature) {
|
|
1618
|
+
requestBody.signature = signature;
|
|
1619
|
+
}
|
|
1609
1620
|
const response = await fetch(`${API_BASE_URL}/api/v1/external/recognition/purpose/`, {
|
|
1610
1621
|
method: "POST",
|
|
1611
1622
|
headers: {
|
|
1612
1623
|
"Content-Type": "application/json",
|
|
1613
1624
|
"X-App-Token": applicationToken
|
|
1614
1625
|
},
|
|
1615
|
-
body: JSON.stringify(
|
|
1616
|
-
biometric_data: base64Image,
|
|
1617
|
-
type_of_identification: "FACE",
|
|
1618
|
-
purpose,
|
|
1619
|
-
confidence_threshold: confidenceThreshold
|
|
1620
|
-
}),
|
|
1626
|
+
body: JSON.stringify(requestBody),
|
|
1621
1627
|
signal: controller.signal
|
|
1622
1628
|
});
|
|
1623
1629
|
if (!response.ok) {
|
|
@@ -1629,10 +1635,16 @@ const recognizeByPurpose = async (image, applicationToken, purpose, confidenceTh
|
|
|
1629
1635
|
const data = await response.json();
|
|
1630
1636
|
return {
|
|
1631
1637
|
success: data.success,
|
|
1632
|
-
|
|
1638
|
+
personName: data.person_name || ((_a2 = data.person) == null ? void 0 : _a2.name),
|
|
1639
|
+
email: data.email || (sessionData == null ? void 0 : sessionData.email),
|
|
1640
|
+
cpf: data.cpf || (sessionData == null ? void 0 : sessionData.cpf),
|
|
1633
1641
|
confidenceScore: data.confidence_score,
|
|
1634
1642
|
accessToken: data.access_token,
|
|
1635
|
-
payload: data.payload
|
|
1643
|
+
payload: data.payload,
|
|
1644
|
+
// Return signature and sessionId for external integrations
|
|
1645
|
+
signature,
|
|
1646
|
+
sessionId: sessionData == null ? void 0 : sessionData.sessionId
|
|
1647
|
+
// Note: personId is intentionally NOT returned for security reasons
|
|
1636
1648
|
};
|
|
1637
1649
|
} catch (error) {
|
|
1638
1650
|
if (error instanceof Error) {
|
|
@@ -1915,6 +1927,79 @@ const identifyPerson = async (image, applicationToken) => {
|
|
|
1915
1927
|
throw new NeoFaceError("Unknown error occurred", ErrorType.NETWORK_ERROR);
|
|
1916
1928
|
}
|
|
1917
1929
|
};
|
|
1930
|
+
const registerBiometric = async (personId, faceImage, applicationToken) => {
|
|
1931
|
+
ensureSecureContext();
|
|
1932
|
+
if (!personId) {
|
|
1933
|
+
throw new NeoFaceError("Person ID is required", ErrorType.VALIDATION_ERROR);
|
|
1934
|
+
}
|
|
1935
|
+
if (!faceImage) {
|
|
1936
|
+
throw new NeoFaceError("Face image is required", ErrorType.VALIDATION_ERROR);
|
|
1937
|
+
}
|
|
1938
|
+
const controller = createTimeoutController();
|
|
1939
|
+
const compressedImage = await compressImage(faceImage);
|
|
1940
|
+
const base64Image = await new Promise((resolve, reject) => {
|
|
1941
|
+
const reader = new FileReader();
|
|
1942
|
+
reader.onload = () => {
|
|
1943
|
+
const result = reader.result;
|
|
1944
|
+
resolve(result.split(",")[1]);
|
|
1945
|
+
};
|
|
1946
|
+
reader.onerror = reject;
|
|
1947
|
+
reader.readAsDataURL(compressedImage);
|
|
1948
|
+
});
|
|
1949
|
+
try {
|
|
1950
|
+
const response = await fetch(`${API_BASE_URL}/api/v1/identity-data/`, {
|
|
1951
|
+
method: "POST",
|
|
1952
|
+
headers: {
|
|
1953
|
+
"Content-Type": "application/json",
|
|
1954
|
+
"X-App-Token": applicationToken
|
|
1955
|
+
},
|
|
1956
|
+
body: JSON.stringify({
|
|
1957
|
+
person: personId,
|
|
1958
|
+
type_of_identification: "FACE",
|
|
1959
|
+
biometric_data: base64Image
|
|
1960
|
+
}),
|
|
1961
|
+
signal: controller.signal
|
|
1962
|
+
});
|
|
1963
|
+
if (!response.ok) {
|
|
1964
|
+
if (response.status === 401 || response.status === 403) {
|
|
1965
|
+
throw new NeoFaceError("Invalid or expired application token, or insufficient permissions", ErrorType.INVALID_TOKEN);
|
|
1966
|
+
}
|
|
1967
|
+
if (response.status === 400) {
|
|
1968
|
+
try {
|
|
1969
|
+
const errorData = await response.json();
|
|
1970
|
+
const errorMessage = errorData.message || errorData.error || "Invalid biometric data";
|
|
1971
|
+
throw new NeoFaceError(errorMessage, ErrorType.VALIDATION_ERROR);
|
|
1972
|
+
} catch (parseError) {
|
|
1973
|
+
if (parseError instanceof NeoFaceError) {
|
|
1974
|
+
throw parseError;
|
|
1975
|
+
}
|
|
1976
|
+
throw new NeoFaceError("Invalid biometric data", ErrorType.VALIDATION_ERROR);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
throw new NeoFaceError(`Server returned status ${response.status}`, ErrorType.NETWORK);
|
|
1980
|
+
}
|
|
1981
|
+
const data = await response.json();
|
|
1982
|
+
return {
|
|
1983
|
+
success: true,
|
|
1984
|
+
message: "Biometric data registered successfully",
|
|
1985
|
+
identity_data_id: data.id,
|
|
1986
|
+
type_of_identification: data.type_of_identification,
|
|
1987
|
+
have_biometric_data: data.have_biometric_data,
|
|
1988
|
+
created_at: data.created_at
|
|
1989
|
+
};
|
|
1990
|
+
} catch (error) {
|
|
1991
|
+
if (error instanceof Error) {
|
|
1992
|
+
if (error.name === "AbortError") {
|
|
1993
|
+
throw new NeoFaceError("Request timed out", ErrorType.NETWORK);
|
|
1994
|
+
}
|
|
1995
|
+
if (error instanceof NeoFaceError) {
|
|
1996
|
+
throw error;
|
|
1997
|
+
}
|
|
1998
|
+
throw new NeoFaceError(error.message, ErrorType.NETWORK);
|
|
1999
|
+
}
|
|
2000
|
+
throw new NeoFaceError("Unknown error occurred", ErrorType.NETWORK_ERROR);
|
|
2001
|
+
}
|
|
2002
|
+
};
|
|
1918
2003
|
const modalReducer$1 = (state, action) => {
|
|
1919
2004
|
switch (action.type) {
|
|
1920
2005
|
case "PERMISSION_GRANTED":
|
|
@@ -4669,6 +4754,116 @@ class EmailPasswordModal {
|
|
|
4669
4754
|
}
|
|
4670
4755
|
const MAX_ATTEMPTS = 3;
|
|
4671
4756
|
const RETRY_DELAY = 500;
|
|
4757
|
+
async function captureFaceSilently() {
|
|
4758
|
+
return new Promise((resolve, reject) => {
|
|
4759
|
+
const video = document.createElement("video");
|
|
4760
|
+
video.style.position = "fixed";
|
|
4761
|
+
video.style.top = "-9999px";
|
|
4762
|
+
video.style.left = "-9999px";
|
|
4763
|
+
video.style.width = "1px";
|
|
4764
|
+
video.style.height = "1px";
|
|
4765
|
+
video.style.opacity = "0";
|
|
4766
|
+
video.setAttribute("autoplay", "");
|
|
4767
|
+
video.setAttribute("muted", "");
|
|
4768
|
+
video.setAttribute("playsinline", "");
|
|
4769
|
+
const canvas = document.createElement("canvas");
|
|
4770
|
+
canvas.style.display = "none";
|
|
4771
|
+
let stream = null;
|
|
4772
|
+
let faceDetectionAttempts = 0;
|
|
4773
|
+
const MAX_FACE_DETECTION_ATTEMPTS = 10;
|
|
4774
|
+
const cleanup = () => {
|
|
4775
|
+
if (stream) {
|
|
4776
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
4777
|
+
}
|
|
4778
|
+
if (video.parentElement) {
|
|
4779
|
+
video.parentElement.removeChild(video);
|
|
4780
|
+
}
|
|
4781
|
+
if (canvas.parentElement) {
|
|
4782
|
+
canvas.parentElement.removeChild(canvas);
|
|
4783
|
+
}
|
|
4784
|
+
};
|
|
4785
|
+
document.body.appendChild(video);
|
|
4786
|
+
document.body.appendChild(canvas);
|
|
4787
|
+
navigator.mediaDevices.getUserMedia({
|
|
4788
|
+
video: {
|
|
4789
|
+
width: { ideal: 640 },
|
|
4790
|
+
height: { ideal: 480 },
|
|
4791
|
+
facingMode: "user"
|
|
4792
|
+
},
|
|
4793
|
+
audio: false
|
|
4794
|
+
}).then((mediaStream) => {
|
|
4795
|
+
stream = mediaStream;
|
|
4796
|
+
video.srcObject = stream;
|
|
4797
|
+
video.play();
|
|
4798
|
+
const tryCapture = () => {
|
|
4799
|
+
if (!video.videoWidth || !video.videoHeight) {
|
|
4800
|
+
if (faceDetectionAttempts < MAX_FACE_DETECTION_ATTEMPTS) {
|
|
4801
|
+
faceDetectionAttempts++;
|
|
4802
|
+
setTimeout(tryCapture, 100);
|
|
4803
|
+
return;
|
|
4804
|
+
}
|
|
4805
|
+
cleanup();
|
|
4806
|
+
reject(new NeoFaceError("Câmera não está pronta", ErrorType.CAMERA_ERROR));
|
|
4807
|
+
return;
|
|
4808
|
+
}
|
|
4809
|
+
canvas.width = video.videoWidth;
|
|
4810
|
+
canvas.height = video.videoHeight;
|
|
4811
|
+
const ctx = canvas.getContext("2d");
|
|
4812
|
+
if (!ctx) {
|
|
4813
|
+
cleanup();
|
|
4814
|
+
reject(new Error("Failed to get canvas context"));
|
|
4815
|
+
return;
|
|
4816
|
+
}
|
|
4817
|
+
const detectFace2 = async () => {
|
|
4818
|
+
try {
|
|
4819
|
+
const detection = await faceapi.detectSingleFace(
|
|
4820
|
+
video,
|
|
4821
|
+
new faceapi.TinyFaceDetectorOptions({ inputSize: 160, scoreThreshold: 0.4 })
|
|
4822
|
+
);
|
|
4823
|
+
return !!detection;
|
|
4824
|
+
} catch {
|
|
4825
|
+
return false;
|
|
4826
|
+
}
|
|
4827
|
+
};
|
|
4828
|
+
detectFace2().then((hasFace) => {
|
|
4829
|
+
if (!hasFace && faceDetectionAttempts < MAX_FACE_DETECTION_ATTEMPTS) {
|
|
4830
|
+
faceDetectionAttempts++;
|
|
4831
|
+
setTimeout(tryCapture, 200);
|
|
4832
|
+
return;
|
|
4833
|
+
}
|
|
4834
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
4835
|
+
canvas.toBlob(
|
|
4836
|
+
(blob) => {
|
|
4837
|
+
cleanup();
|
|
4838
|
+
if (blob) {
|
|
4839
|
+
resolve(blob);
|
|
4840
|
+
} else {
|
|
4841
|
+
reject(new Error("Failed to capture image"));
|
|
4842
|
+
}
|
|
4843
|
+
},
|
|
4844
|
+
"image/jpeg",
|
|
4845
|
+
0.85
|
|
4846
|
+
);
|
|
4847
|
+
});
|
|
4848
|
+
};
|
|
4849
|
+
video.onloadedmetadata = () => {
|
|
4850
|
+
setTimeout(tryCapture, 800);
|
|
4851
|
+
};
|
|
4852
|
+
video.onerror = () => {
|
|
4853
|
+
cleanup();
|
|
4854
|
+
reject(new NeoFaceError("Erro ao acessar câmera", ErrorType.CAMERA_ERROR));
|
|
4855
|
+
};
|
|
4856
|
+
}).catch((error) => {
|
|
4857
|
+
cleanup();
|
|
4858
|
+
reject(
|
|
4859
|
+
new NeoFaceError(
|
|
4860
|
+
"Não foi possível acessar a câmera: " + error.message,
|
|
4861
|
+
ErrorType.CAMERA_ERROR
|
|
4862
|
+
)
|
|
4863
|
+
);
|
|
4864
|
+
});
|
|
4865
|
+
});
|
|
4866
|
+
}
|
|
4672
4867
|
async function detectFaceContinuously(video, minDetectionTime = 3e3, detectionInterval = 200) {
|
|
4673
4868
|
return new Promise((resolve) => {
|
|
4674
4869
|
let faceDetectedCount = 0;
|
|
@@ -4891,6 +5086,11 @@ async function executeBiometricLoginFlow(options) {
|
|
|
4891
5086
|
tryLogin();
|
|
4892
5087
|
}, 300);
|
|
4893
5088
|
}
|
|
5089
|
+
const biometricLoginFlow = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
5090
|
+
__proto__: null,
|
|
5091
|
+
captureFaceSilently,
|
|
5092
|
+
executeBiometricLoginFlow
|
|
5093
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
4894
5094
|
async function startFaceLogin(options) {
|
|
4895
5095
|
try {
|
|
4896
5096
|
const isValidToken = await validateToken(options.applicationToken);
|
|
@@ -5218,8 +5418,8 @@ const biometricDetection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.
|
|
|
5218
5418
|
initializeBiometricDetection,
|
|
5219
5419
|
isAdvancedDetectionAvailable
|
|
5220
5420
|
}, Symbol.toStringTag, { value: "Module" }));
|
|
5221
|
-
const VERSION = "1.
|
|
5222
|
-
const RELEASE_DATE = "2025-
|
|
5421
|
+
const VERSION = "1.6.1";
|
|
5422
|
+
const RELEASE_DATE = "2025-12-01";
|
|
5223
5423
|
class OnboardingCaptureModal {
|
|
5224
5424
|
constructor(options) {
|
|
5225
5425
|
__publicField(this, "overlay", null);
|
|
@@ -5443,6 +5643,284 @@ const startOnboarding = async (options) => {
|
|
|
5443
5643
|
onError(new NeoFaceError(message, ErrorType.UNKNOWN));
|
|
5444
5644
|
}
|
|
5445
5645
|
};
|
|
5646
|
+
class NeoFaceID {
|
|
5647
|
+
/**
|
|
5648
|
+
* Creates a new NeoFaceID instance
|
|
5649
|
+
* @param config Configuration object
|
|
5650
|
+
* @throws NeoFaceError if signature format is invalid
|
|
5651
|
+
*/
|
|
5652
|
+
constructor(config) {
|
|
5653
|
+
__publicField(this, "appToken");
|
|
5654
|
+
__publicField(this, "baseUrl");
|
|
5655
|
+
__publicField(this, "signature");
|
|
5656
|
+
__publicField(this, "sessionData");
|
|
5657
|
+
var _a2;
|
|
5658
|
+
this.appToken = config.appToken;
|
|
5659
|
+
this.baseUrl = config.baseUrl || (((_a2 = { "BASE_URL": "/", "MODE": "production", "DEV": false, "PROD": true, "SSR": false }) == null ? void 0 : _a2.VITE_API_BASE_URL) || "http://localhost:8000");
|
|
5660
|
+
this.signature = config.signature;
|
|
5661
|
+
this.sessionData = config.sessionData;
|
|
5662
|
+
if (this.signature && !this.validateSignatureFormat(this.signature)) {
|
|
5663
|
+
throw new NeoFaceError(
|
|
5664
|
+
"Invalid signature format. Expected HMAC-SHA256 hex string (minimum 64 characters)",
|
|
5665
|
+
ErrorType.VALIDATION_ERROR
|
|
5666
|
+
);
|
|
5667
|
+
}
|
|
5668
|
+
if (this.sessionData) {
|
|
5669
|
+
if (!this.sessionData.email || !this.sessionData.cpf || !this.sessionData.sessionId) {
|
|
5670
|
+
throw new NeoFaceError(
|
|
5671
|
+
"Session data must include email, cpf, and sessionId",
|
|
5672
|
+
ErrorType.VALIDATION_ERROR
|
|
5673
|
+
);
|
|
5674
|
+
}
|
|
5675
|
+
}
|
|
5676
|
+
}
|
|
5677
|
+
/**
|
|
5678
|
+
* Validates the format of a signature
|
|
5679
|
+
* HMAC-SHA256 produces a 64-character hexadecimal string
|
|
5680
|
+
* @param signature The signature to validate
|
|
5681
|
+
* @returns true if valid, false otherwise
|
|
5682
|
+
*/
|
|
5683
|
+
validateSignatureFormat(signature) {
|
|
5684
|
+
return /^[a-f0-9]{64,}$/i.test(signature);
|
|
5685
|
+
}
|
|
5686
|
+
/**
|
|
5687
|
+
* Captures multiple face frames for biometric recognition
|
|
5688
|
+
* @param options Capture options
|
|
5689
|
+
* @returns Promise that resolves to an array of image blobs
|
|
5690
|
+
* @throws NeoFaceError if capture fails
|
|
5691
|
+
*/
|
|
5692
|
+
async captureFaceFrames(options) {
|
|
5693
|
+
const { numFrames, livenessCheck } = options;
|
|
5694
|
+
if (numFrames < 1 || numFrames > 10) {
|
|
5695
|
+
throw new NeoFaceError(
|
|
5696
|
+
"Number of frames must be between 1 and 10",
|
|
5697
|
+
ErrorType.VALIDATION_ERROR
|
|
5698
|
+
);
|
|
5699
|
+
}
|
|
5700
|
+
const frames = [];
|
|
5701
|
+
try {
|
|
5702
|
+
const { captureFaceSilently: captureFaceSilently2 } = await Promise.resolve().then(() => biometricLoginFlow);
|
|
5703
|
+
for (let i = 0; i < numFrames; i++) {
|
|
5704
|
+
const frame = await captureFaceSilently2();
|
|
5705
|
+
frames.push(frame);
|
|
5706
|
+
if (livenessCheck && i < numFrames - 1) {
|
|
5707
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
5708
|
+
}
|
|
5709
|
+
}
|
|
5710
|
+
return frames;
|
|
5711
|
+
} catch (error) {
|
|
5712
|
+
if (error instanceof NeoFaceError) {
|
|
5713
|
+
throw error;
|
|
5714
|
+
}
|
|
5715
|
+
throw new NeoFaceError(
|
|
5716
|
+
`Failed to capture face frames: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
5717
|
+
ErrorType.CAPTURE_ERROR
|
|
5718
|
+
);
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5721
|
+
/**
|
|
5722
|
+
* Performs login recognition using biometric data
|
|
5723
|
+
*
|
|
5724
|
+
* This method is designed for external integrations that require
|
|
5725
|
+
* signature validation and session management.
|
|
5726
|
+
*
|
|
5727
|
+
* @param options Recognition options
|
|
5728
|
+
* @returns Promise that resolves to recognition result
|
|
5729
|
+
* @throws NeoFaceError if recognition fails
|
|
5730
|
+
*
|
|
5731
|
+
* @example
|
|
5732
|
+
* ```typescript
|
|
5733
|
+
* const result = await sdk.loginRecognition({
|
|
5734
|
+
* biometricData: await sdk.captureFaceFrames({
|
|
5735
|
+
* numFrames: 5,
|
|
5736
|
+
* livenessCheck: true
|
|
5737
|
+
* }),
|
|
5738
|
+
* typeOfIdentification: 'FACE',
|
|
5739
|
+
* purpose: 'LOGIN',
|
|
5740
|
+
* confidenceThreshold: 0.8
|
|
5741
|
+
* });
|
|
5742
|
+
*
|
|
5743
|
+
* // Result includes signature and sessionId for callback validation
|
|
5744
|
+
* console.log(result.signature, result.sessionId);
|
|
5745
|
+
* ```
|
|
5746
|
+
*/
|
|
5747
|
+
async loginRecognition(options) {
|
|
5748
|
+
const {
|
|
5749
|
+
biometricData,
|
|
5750
|
+
typeOfIdentification,
|
|
5751
|
+
purpose,
|
|
5752
|
+
confidenceThreshold = 0.8
|
|
5753
|
+
} = options;
|
|
5754
|
+
const imageBlobs = Array.isArray(biometricData) ? biometricData : [biometricData];
|
|
5755
|
+
if (imageBlobs.length === 0) {
|
|
5756
|
+
throw new NeoFaceError(
|
|
5757
|
+
"At least one biometric data frame is required",
|
|
5758
|
+
ErrorType.VALIDATION_ERROR
|
|
5759
|
+
);
|
|
5760
|
+
}
|
|
5761
|
+
const primaryImage = imageBlobs[0];
|
|
5762
|
+
try {
|
|
5763
|
+
const result = await recognizeByPurpose(
|
|
5764
|
+
primaryImage,
|
|
5765
|
+
this.appToken,
|
|
5766
|
+
purpose,
|
|
5767
|
+
confidenceThreshold,
|
|
5768
|
+
this.signature,
|
|
5769
|
+
this.sessionData
|
|
5770
|
+
);
|
|
5771
|
+
if (!result.success) {
|
|
5772
|
+
throw new NeoFaceError(
|
|
5773
|
+
"Recognition failed",
|
|
5774
|
+
ErrorType.RECOGNITION_FAILED
|
|
5775
|
+
);
|
|
5776
|
+
}
|
|
5777
|
+
if (!result.personName || !result.email || !result.cpf) {
|
|
5778
|
+
throw new NeoFaceError(
|
|
5779
|
+
"Incomplete recognition result: missing personName, email, or cpf",
|
|
5780
|
+
ErrorType.RECOGNITION_FAILED
|
|
5781
|
+
);
|
|
5782
|
+
}
|
|
5783
|
+
return {
|
|
5784
|
+
success: true,
|
|
5785
|
+
personName: result.personName,
|
|
5786
|
+
email: result.email,
|
|
5787
|
+
cpf: result.cpf,
|
|
5788
|
+
signature: result.signature,
|
|
5789
|
+
sessionId: result.sessionId,
|
|
5790
|
+
confidenceScore: result.confidenceScore,
|
|
5791
|
+
accessToken: result.accessToken
|
|
5792
|
+
};
|
|
5793
|
+
} catch (error) {
|
|
5794
|
+
if (error instanceof NeoFaceError) {
|
|
5795
|
+
throw error;
|
|
5796
|
+
}
|
|
5797
|
+
throw new NeoFaceError(
|
|
5798
|
+
`Login recognition failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
5799
|
+
ErrorType.RECOGNITION_FAILED
|
|
5800
|
+
);
|
|
5801
|
+
}
|
|
5802
|
+
}
|
|
5803
|
+
}
|
|
5804
|
+
const PHOTO_INSTRUCTIONS = [
|
|
5805
|
+
"Olhe diretamente para a câmera",
|
|
5806
|
+
"Vire levemente a cabeça para a esquerda",
|
|
5807
|
+
"Vire levemente a cabeça para a direita",
|
|
5808
|
+
"Mostre o polegar para cima (👍) e olhe para a câmera"
|
|
5809
|
+
];
|
|
5810
|
+
const TOTAL_PHOTOS = 4;
|
|
5811
|
+
const authorizeOperation = async (applicationToken, cpf, options) => {
|
|
5812
|
+
if (!applicationToken) {
|
|
5813
|
+
throw new NeoFaceError("Application token is required", ErrorType.VALIDATION_ERROR);
|
|
5814
|
+
}
|
|
5815
|
+
if (!cpf) {
|
|
5816
|
+
throw new NeoFaceError("CPF is required", ErrorType.VALIDATION_ERROR);
|
|
5817
|
+
}
|
|
5818
|
+
const cpfClean = cpf.replace(/\D/g, "");
|
|
5819
|
+
if (cpfClean.length !== 11) {
|
|
5820
|
+
throw new NeoFaceError("Invalid CPF format", ErrorType.VALIDATION_ERROR);
|
|
5821
|
+
}
|
|
5822
|
+
const capturedPhotos = [];
|
|
5823
|
+
const captureDelay = (options == null ? void 0 : options.captureDelay) || 2e3;
|
|
5824
|
+
try {
|
|
5825
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
5826
|
+
throw new NeoFaceError("Camera not available", ErrorType.NO_CAMERA);
|
|
5827
|
+
}
|
|
5828
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
5829
|
+
video: {
|
|
5830
|
+
facingMode: "user",
|
|
5831
|
+
width: { ideal: 1280 },
|
|
5832
|
+
height: { ideal: 720 }
|
|
5833
|
+
}
|
|
5834
|
+
});
|
|
5835
|
+
const video = document.createElement("video");
|
|
5836
|
+
video.srcObject = stream;
|
|
5837
|
+
video.autoplay = true;
|
|
5838
|
+
video.playsInline = true;
|
|
5839
|
+
await new Promise((resolve) => {
|
|
5840
|
+
video.onloadedmetadata = () => {
|
|
5841
|
+
video.play();
|
|
5842
|
+
resolve();
|
|
5843
|
+
};
|
|
5844
|
+
});
|
|
5845
|
+
const canvas = document.createElement("canvas");
|
|
5846
|
+
canvas.width = video.videoWidth;
|
|
5847
|
+
canvas.height = video.videoHeight;
|
|
5848
|
+
const ctx = canvas.getContext("2d");
|
|
5849
|
+
if (!ctx) {
|
|
5850
|
+
throw new NeoFaceError("Failed to get canvas context", ErrorType.CAPTURE_ERROR);
|
|
5851
|
+
}
|
|
5852
|
+
for (let i = 0; i < TOTAL_PHOTOS; i++) {
|
|
5853
|
+
const instruction = PHOTO_INSTRUCTIONS[i];
|
|
5854
|
+
if (options == null ? void 0 : options.onProgress) {
|
|
5855
|
+
options.onProgress(i + 1, TOTAL_PHOTOS, instruction);
|
|
5856
|
+
}
|
|
5857
|
+
if (i > 0) {
|
|
5858
|
+
await new Promise((resolve) => setTimeout(resolve, captureDelay));
|
|
5859
|
+
}
|
|
5860
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
5861
|
+
const blob = await new Promise((resolve, reject) => {
|
|
5862
|
+
canvas.toBlob(
|
|
5863
|
+
(blob2) => {
|
|
5864
|
+
if (blob2) {
|
|
5865
|
+
resolve(blob2);
|
|
5866
|
+
} else {
|
|
5867
|
+
reject(new Error("Failed to capture photo"));
|
|
5868
|
+
}
|
|
5869
|
+
},
|
|
5870
|
+
"image/jpeg",
|
|
5871
|
+
0.9
|
|
5872
|
+
);
|
|
5873
|
+
});
|
|
5874
|
+
capturedPhotos.push(blob);
|
|
5875
|
+
if (options == null ? void 0 : options.onPhotoTaken) {
|
|
5876
|
+
options.onPhotoTaken(i + 1, blob);
|
|
5877
|
+
}
|
|
5878
|
+
}
|
|
5879
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
5880
|
+
const lastPhoto = capturedPhotos[TOTAL_PHOTOS - 1];
|
|
5881
|
+
const sessionData = {
|
|
5882
|
+
cpf: cpfClean,
|
|
5883
|
+
email: "",
|
|
5884
|
+
// Not required for authorization
|
|
5885
|
+
sessionId: `auth_${Date.now()}_${Math.random().toString(36).substring(7)}`
|
|
5886
|
+
};
|
|
5887
|
+
const recognitionResult = await recognizeByPurpose(
|
|
5888
|
+
lastPhoto,
|
|
5889
|
+
applicationToken,
|
|
5890
|
+
"AUTHORIZATION",
|
|
5891
|
+
0.8,
|
|
5892
|
+
// Confidence threshold
|
|
5893
|
+
void 0,
|
|
5894
|
+
// No signature for now
|
|
5895
|
+
sessionData
|
|
5896
|
+
);
|
|
5897
|
+
if (!recognitionResult.success) {
|
|
5898
|
+
throw new NeoFaceError(
|
|
5899
|
+
"Authorization failed: Person not recognized",
|
|
5900
|
+
ErrorType.RECOGNITION_FAILED
|
|
5901
|
+
);
|
|
5902
|
+
}
|
|
5903
|
+
return {
|
|
5904
|
+
success: true,
|
|
5905
|
+
authorizationHash: sessionData.sessionId,
|
|
5906
|
+
// TEMPORARY: Using sessionId as hash
|
|
5907
|
+
name: recognitionResult.personName,
|
|
5908
|
+
birthDate: void 0,
|
|
5909
|
+
// Should come from backend
|
|
5910
|
+
cpf: recognitionResult.cpf || cpfClean,
|
|
5911
|
+
confidenceScore: recognitionResult.confidenceScore,
|
|
5912
|
+
message: "Authorization successful"
|
|
5913
|
+
};
|
|
5914
|
+
} catch (error) {
|
|
5915
|
+
if (error instanceof NeoFaceError) {
|
|
5916
|
+
throw error;
|
|
5917
|
+
}
|
|
5918
|
+
if (error instanceof Error) {
|
|
5919
|
+
throw new NeoFaceError(error.message, ErrorType.CAPTURE_ERROR);
|
|
5920
|
+
}
|
|
5921
|
+
throw new NeoFaceError("Unknown error during authorization", ErrorType.UNKNOWN);
|
|
5922
|
+
}
|
|
5923
|
+
};
|
|
5446
5924
|
function start(applicationToken, callbacks) {
|
|
5447
5925
|
const container = document.createElement("div");
|
|
5448
5926
|
container.id = "neoface-modal-container";
|
|
@@ -5513,8 +5991,10 @@ export {
|
|
|
5513
5991
|
FaceCaptureModal,
|
|
5514
5992
|
FallbackPrompt,
|
|
5515
5993
|
NeoFaceError,
|
|
5994
|
+
NeoFaceID,
|
|
5516
5995
|
RELEASE_DATE,
|
|
5517
5996
|
VERSION,
|
|
5997
|
+
authorizeOperation,
|
|
5518
5998
|
biometricLogin,
|
|
5519
5999
|
biometricLoginWithFallback,
|
|
5520
6000
|
completeOnboarding,
|
|
@@ -5527,6 +6007,7 @@ export {
|
|
|
5527
6007
|
recognize,
|
|
5528
6008
|
recognizeBiometric,
|
|
5529
6009
|
recognizeByPurpose,
|
|
6010
|
+
registerBiometric,
|
|
5530
6011
|
registerPersonWithBiometric,
|
|
5531
6012
|
registerPersonWithoutFace,
|
|
5532
6013
|
simpleIdentification,
|