@neofaceid/web-sdk 1.44.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -24
- package/dist/biometricDetection-8utmtR25.js +154 -0
- package/dist/index.d.ts +83 -327
- package/dist/neoface-id-sdk.es.js +783 -1192
- package/dist/neoface-id-sdk.umd.js +1 -1
- package/dist/qrcode-DYfGiWu6.js +1633 -0
- package/package.json +2 -4
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Veja o [`CHANGELOG.md`](CHANGELOG.md) para o histórico de versões.
|
|
|
20
20
|
- [Onboarding completo (`startOnboarding`)](#onboarding-completo-startonboarding)
|
|
21
21
|
- [Captura de documento (`startDocumentCapture`)](#captura-de-documento-startdocumentcapture)
|
|
22
22
|
- [Autorização por biometria (`authorize` / `authorizeOperation`)](#autorização-por-biometria-authorize--authorizeoperation)
|
|
23
|
-
- [
|
|
23
|
+
- [Prova de vida, captura de frames e operações avulsas](#prova-de-vida-captura-de-frames-e-operações-avulsas)
|
|
24
24
|
- [Tratamento de erros](#tratamento-de-erros)
|
|
25
25
|
- [Componentes React exportados](#componentes-react-exportados)
|
|
26
26
|
- [Sessão de captura e desafio de liveness (baixo nível)](#sessão-de-captura-e-desafio-de-liveness-baixo-nível)
|
|
@@ -289,44 +289,47 @@ const result = await authorizeOperation('seu-token-de-aplicacao', '12345678900',
|
|
|
289
289
|
});
|
|
290
290
|
```
|
|
291
291
|
|
|
292
|
-
###
|
|
292
|
+
### Prova de vida, captura de frames e operações avulsas
|
|
293
293
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
registro de documento pós-cadastro.
|
|
294
|
+
Operações que não abrem um fluxo `start*` completo. Até a 1.x viviam na
|
|
295
|
+
classe `NeoFaceID`; desde a 2.0.0 são funções, como o resto do SDK — ver a
|
|
296
|
+
tabela de migração no [CHANGELOG](CHANGELOG.md#200---2026-09-19--neo-244-neo-245).
|
|
298
297
|
|
|
299
298
|
```ts
|
|
300
|
-
import {
|
|
301
|
-
|
|
302
|
-
|
|
299
|
+
import {
|
|
300
|
+
proofOfLife,
|
|
301
|
+
captureFaceFrames,
|
|
302
|
+
recognizeByPurpose,
|
|
303
|
+
registerDocumentByImage,
|
|
304
|
+
} from '@neofaceid/web-sdk';
|
|
303
305
|
|
|
304
|
-
// Prova de vida: grava vídeo curto, envia
|
|
305
|
-
|
|
306
|
+
// Prova de vida: grava vídeo curto, envia ao core e acompanha até o resultado.
|
|
307
|
+
// `applicationToken` pode ser omitido se já foi passado a init().
|
|
308
|
+
const proof = await proofOfLife({
|
|
309
|
+
applicationToken: 'seu-token-de-aplicacao',
|
|
306
310
|
videoDurationMs: 3000,
|
|
307
311
|
onRecordingProgress: progress => console.log(`Gravando: ${progress}%`),
|
|
308
312
|
onTaskStatusChange: (status, progress) => console.log(status, progress),
|
|
309
313
|
});
|
|
314
|
+
// Reprovar na prova é desfecho, não exceção: confira `success` e `isLive`.
|
|
310
315
|
if (proof.success && proof.isLive) {
|
|
311
316
|
console.log('Liveness score:', proof.livenessScore);
|
|
312
317
|
console.log('Dados da pessoa:', proof.personalData);
|
|
313
318
|
}
|
|
314
319
|
|
|
315
|
-
// Login com assinatura (integrações externas
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
purpose: 'LOGIN',
|
|
326
|
-
});
|
|
320
|
+
// Login com assinatura (integrações externas que já geram HMAC)
|
|
321
|
+
const [frame] = await captureFaceFrames({ numFrames: 1, livenessCheck: false });
|
|
322
|
+
const login = await recognizeByPurpose(
|
|
323
|
+
frame,
|
|
324
|
+
'seu-token-de-aplicacao',
|
|
325
|
+
'LOGIN',
|
|
326
|
+
0.8, // limiar de confiança
|
|
327
|
+
signatureFromBackend, // HMAC-SHA256 hex, mínimo 64 caracteres
|
|
328
|
+
{ email: 'user@exemplo.com', cpf: '12345678900', sessionId: 'session-uuid' }
|
|
329
|
+
);
|
|
327
330
|
|
|
328
331
|
// Registro de documento para pessoa já cadastrada
|
|
329
|
-
const docResult = await
|
|
332
|
+
const docResult = await registerDocumentByImage(personId, userJwtToken);
|
|
330
333
|
console.log('Task ID:', docResult.taskId);
|
|
331
334
|
```
|
|
332
335
|
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
async function detectBiometricType(imageData) {
|
|
2
|
+
try {
|
|
3
|
+
const img = await loadImageFromBase64(imageData);
|
|
4
|
+
const faceResult = await detectFace(img);
|
|
5
|
+
if (faceResult.confidence > 0.7) {
|
|
6
|
+
return {
|
|
7
|
+
type: "face",
|
|
8
|
+
confidence: faceResult.confidence,
|
|
9
|
+
details: faceResult.details
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
const handResult = await detectHand(img);
|
|
13
|
+
if (handResult.confidence > 0.6) {
|
|
14
|
+
return {
|
|
15
|
+
type: "hand",
|
|
16
|
+
confidence: handResult.confidence,
|
|
17
|
+
details: handResult.details
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
type: "unknown",
|
|
22
|
+
confidence: Math.max(faceResult.confidence, handResult.confidence)
|
|
23
|
+
};
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.warn("Erro na detecção biométrica:", error);
|
|
26
|
+
return {
|
|
27
|
+
type: "face",
|
|
28
|
+
confidence: 0.5
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function detectFace(img) {
|
|
33
|
+
try {
|
|
34
|
+
if (typeof window !== "undefined" && window.faceapi) {
|
|
35
|
+
const { faceapi } = window;
|
|
36
|
+
await loadFaceApiModels();
|
|
37
|
+
const tinyOptions = new faceapi.TinyFaceDetectorOptions({
|
|
38
|
+
inputSize: 320,
|
|
39
|
+
scoreThreshold: 0.5
|
|
40
|
+
});
|
|
41
|
+
const detections = await faceapi.detectAllFaces(img, tinyOptions).withFaceLandmarks();
|
|
42
|
+
if (detections && detections.length > 0) {
|
|
43
|
+
const bestDetection = detections.reduce(
|
|
44
|
+
(best, current) => current.detection.score > best.detection.score ? current : best
|
|
45
|
+
);
|
|
46
|
+
return {
|
|
47
|
+
confidence: bestDetection.detection.score,
|
|
48
|
+
details: {
|
|
49
|
+
faces: detections.length,
|
|
50
|
+
landmarks: bestDetection.landmarks,
|
|
51
|
+
box: bestDetection.detection.box
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { confidence: 0 };
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.warn("Erro na detecção facial:", error);
|
|
59
|
+
return { confidence: 0 };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function detectHand(img) {
|
|
63
|
+
try {
|
|
64
|
+
const canvas = document.createElement("canvas");
|
|
65
|
+
const ctx = canvas.getContext("2d");
|
|
66
|
+
if (!ctx) {
|
|
67
|
+
return { confidence: 0 };
|
|
68
|
+
}
|
|
69
|
+
canvas.width = img.width;
|
|
70
|
+
canvas.height = img.height;
|
|
71
|
+
ctx.drawImage(img, 0, 0);
|
|
72
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
73
|
+
const handFeatures = analyzeHandFeatures(imageData);
|
|
74
|
+
return {
|
|
75
|
+
confidence: handFeatures.confidence,
|
|
76
|
+
details: handFeatures
|
|
77
|
+
};
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.warn("Erro na detecção de mão:", error);
|
|
80
|
+
return { confidence: 0 };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function analyzeHandFeatures(imageData) {
|
|
84
|
+
const { data, width, height } = imageData;
|
|
85
|
+
let skinPixels = 0;
|
|
86
|
+
let totalPixels = 0;
|
|
87
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
88
|
+
const r = data[i];
|
|
89
|
+
const g = data[i + 1];
|
|
90
|
+
const b = data[i + 2];
|
|
91
|
+
if (isSkinColor(r, g, b)) {
|
|
92
|
+
skinPixels++;
|
|
93
|
+
}
|
|
94
|
+
totalPixels++;
|
|
95
|
+
}
|
|
96
|
+
const skinRatio = skinPixels / totalPixels;
|
|
97
|
+
const aspectRatio = width / height;
|
|
98
|
+
const isHandAspectRatio = aspectRatio > 0.6 && aspectRatio < 1.8;
|
|
99
|
+
let confidence = 0;
|
|
100
|
+
if (skinRatio > 0.15 && skinRatio < 0.7) {
|
|
101
|
+
confidence += 0.4 * (skinRatio / 0.7);
|
|
102
|
+
}
|
|
103
|
+
if (isHandAspectRatio) {
|
|
104
|
+
confidence += 0.3;
|
|
105
|
+
}
|
|
106
|
+
const imageSize = width * height;
|
|
107
|
+
if (imageSize > 1e4 && imageSize < 5e5) {
|
|
108
|
+
confidence += 0.3;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
confidence: Math.min(confidence, 0.8),
|
|
112
|
+
// Máximo 80% para detecção básica
|
|
113
|
+
skinRatio,
|
|
114
|
+
aspectRatio,
|
|
115
|
+
imageSize,
|
|
116
|
+
skinPixels,
|
|
117
|
+
totalPixels
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function isSkinColor(r, g, b) {
|
|
121
|
+
const y = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
122
|
+
const cb = -0.169 * r - 0.331 * g + 0.5 * b + 128;
|
|
123
|
+
const cr = 0.5 * r - 0.419 * g - 0.081 * b + 128;
|
|
124
|
+
return y > 80 && y < 255 && cb > 85 && cb < 135 && cr > 135 && cr < 180;
|
|
125
|
+
}
|
|
126
|
+
function loadImageFromBase64(base64Data) {
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
const img = new Image();
|
|
129
|
+
img.onload = () => resolve(img);
|
|
130
|
+
img.onerror = () => reject(new Error("Erro ao carregar imagem"));
|
|
131
|
+
const dataUrl = base64Data.startsWith("data:") ? base64Data : `data:image/jpeg;base64,${base64Data}`;
|
|
132
|
+
img.src = dataUrl;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async function loadFaceApiModels() {
|
|
136
|
+
if (typeof window === "undefined" || !window.faceapi) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const { faceapi } = window;
|
|
140
|
+
if (faceapi.nets.tinyFaceDetector.isLoaded) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
await Promise.all([
|
|
145
|
+
faceapi.nets.tinyFaceDetector.loadFromUri("/models"),
|
|
146
|
+
faceapi.nets.faceLandmark68Net.loadFromUri("/models")
|
|
147
|
+
]);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.warn("Não foi possível carregar modelos do face-api.js:", error);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export {
|
|
153
|
+
detectBiometricType
|
|
154
|
+
};
|