@trustchex/react-native-sdk 1.475.1 → 1.481.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.
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +76 -24
- package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +1 -0
- package/ios/Camera/TrustchexCameraView.swift +20 -15
- package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +3 -2
- package/lib/module/Screens/Static/QrCodeScanningScreen.js +12 -2
- package/lib/module/Screens/Static/ResultScreen.js +145 -113
- package/lib/module/Shared/Components/DebugOverlay.js +5 -2
- package/lib/module/Shared/Components/DiagnosticReportButton.js +64 -0
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +185 -30
- package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +8 -1
- package/lib/module/Shared/Components/NavigationManager.js +7 -9
- package/lib/module/Shared/EIDReader/eidReader.js +159 -38
- package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +41 -0
- package/lib/module/Shared/Libs/SignalingClient.js +6 -5
- package/lib/module/Shared/Libs/crypto.utils.js +25 -3
- package/lib/module/Shared/Libs/deeplink.utils.js +37 -7
- package/lib/module/Shared/Libs/diagnosticReport.js +133 -0
- package/lib/module/Shared/Libs/diagnostics.js +171 -0
- package/lib/module/Shared/Libs/http-client.js +19 -6
- package/lib/module/Shared/Libs/mrz.utils.js +193 -33
- package/lib/module/Shared/Libs/mrzFrameAggregator.js +74 -3
- package/lib/module/Shared/Libs/native-device-info.utils.js +67 -0
- package/lib/module/Shared/Libs/promise.utils.js +4 -3
- package/lib/module/Shared/Libs/sendDiagnosticReport.js +130 -0
- package/lib/module/Shared/Libs/shuffle.utils.js +17 -0
- package/lib/module/Shared/Services/AnalyticsService.js +8 -2
- package/lib/module/Shared/Services/DataUploadService.js +114 -127
- package/lib/module/Shared/Services/VideoSessionService.js +4 -3
- package/lib/module/Translation/Resources/en.js +6 -0
- package/lib/module/Translation/Resources/tr.js +6 -0
- package/lib/module/Trustchex.js +20 -4
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DebugOverlay.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts +20 -0
- package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts +10 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts +10 -0
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/crypto.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts +1 -0
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts +48 -0
- package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/diagnostics.d.ts +145 -0
- package/lib/typescript/src/Shared/Libs/diagnostics.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/http-client.d.ts +2 -0
- package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +55 -0
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts +33 -0
- package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts +30 -0
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts +3 -0
- package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +6 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +6 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +17 -3
- package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +4 -3
- package/src/Screens/Static/QrCodeScanningScreen.tsx +19 -2
- package/src/Screens/Static/ResultScreen.tsx +172 -162
- package/src/Shared/Components/DebugOverlay.tsx +5 -2
- package/src/Shared/Components/DiagnosticReportButton.tsx +77 -0
- package/src/Shared/Components/IdentityDocumentCamera.tsx +200 -29
- package/src/Shared/Components/IdentityDocumentCamera.types.ts +6 -0
- package/src/Shared/Components/IdentityDocumentCamera.utils.ts +9 -1
- package/src/Shared/Components/NavigationManager.tsx +7 -7
- package/src/Shared/Components/TrustchexCamera.tsx +6 -0
- package/src/Shared/EIDReader/eidReader.ts +181 -52
- package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +41 -0
- package/src/Shared/Libs/SignalingClient.ts +10 -5
- package/src/Shared/Libs/crypto.utils.ts +41 -4
- package/src/Shared/Libs/deeplink.utils.ts +39 -7
- package/src/Shared/Libs/diagnosticReport.ts +206 -0
- package/src/Shared/Libs/diagnostics.ts +251 -0
- package/src/Shared/Libs/http-client.ts +24 -8
- package/src/Shared/Libs/mrz.utils.ts +200 -34
- package/src/Shared/Libs/mrzFrameAggregator.ts +112 -2
- package/src/Shared/Libs/native-device-info.utils.ts +116 -0
- package/src/Shared/Libs/promise.utils.ts +11 -5
- package/src/Shared/Libs/sendDiagnosticReport.ts +165 -0
- package/src/Shared/Libs/shuffle.utils.ts +15 -0
- package/src/Shared/Services/AnalyticsService.ts +17 -1
- package/src/Shared/Services/DataUploadService.ts +155 -166
- package/src/Shared/Services/VideoSessionService.ts +15 -3
- package/src/Translation/Resources/en.ts +7 -0
- package/src/Translation/Resources/tr.ts +7 -0
- package/src/Trustchex.tsx +26 -3
- package/src/version.ts +1 -1
|
@@ -8,6 +8,34 @@ import { DG1File } from "./lds/icao/dg1File.js";
|
|
|
8
8
|
import { DG2File } from "./lds/icao/dg2File.js";
|
|
9
9
|
import { Buffer } from 'buffer';
|
|
10
10
|
import { InputStream } from "./java/inputStream.js";
|
|
11
|
+
import { debugLog } from "../Libs/debug.utils.js";
|
|
12
|
+
import { diagnostics } from "../Libs/diagnostics.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extract an APDU status word (hex, e.g. "6982") from an error message if the
|
|
16
|
+
* protocol layer included one. Returns undefined when none is present. Kept
|
|
17
|
+
* deliberately loose — the protocol senders embed "SW=...." / "(6982)" forms.
|
|
18
|
+
*/
|
|
19
|
+
const extractStatusWord = msg => {
|
|
20
|
+
const m = msg.match(/\b(?:SW[:= ]?)?([0-9a-fA-F]{4})\b/);
|
|
21
|
+
return m ? m[1].toUpperCase() : undefined;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Categorise an NFC error message into a coarse, non-PII bucket. */
|
|
25
|
+
const categorizeNfcError = msg => {
|
|
26
|
+
const m = msg.toLowerCase();
|
|
27
|
+
if (m.includes('cancel')) return 'user_cancelled';
|
|
28
|
+
if (m.includes('lost') || m.includes('steady')) return 'tag_lost';
|
|
29
|
+
if (m.includes('timeout')) return 'timeout';
|
|
30
|
+
if (m.includes('not supported') || m.includes('unsupported')) {
|
|
31
|
+
return 'unsupported';
|
|
32
|
+
}
|
|
33
|
+
if (m.includes('not enabled') || m.includes('disabled')) return 'not_enabled';
|
|
34
|
+
if (m.includes('auth') || m.includes('mutual') || m.includes('security')) {
|
|
35
|
+
return 'auth_failed';
|
|
36
|
+
}
|
|
37
|
+
return 'reading_error';
|
|
38
|
+
};
|
|
11
39
|
|
|
12
40
|
// --- Minimal PNG encoder (pure JS, no native deps) ---
|
|
13
41
|
// Uses deflate stored (uncompressed) blocks for O(n) encoding speed.
|
|
@@ -147,7 +175,7 @@ function convertJP2IfNeeded(imageBuffer, mimeType) {
|
|
|
147
175
|
mimeType
|
|
148
176
|
};
|
|
149
177
|
}
|
|
150
|
-
|
|
178
|
+
debugLog('EID', '[EID] Face image is JP2, attempting conversion…');
|
|
151
179
|
try {
|
|
152
180
|
// Lazy-load jpeg2000 to avoid crashing if it fails to import
|
|
153
181
|
const {
|
|
@@ -156,16 +184,16 @@ function convertJP2IfNeeded(imageBuffer, mimeType) {
|
|
|
156
184
|
const jpx = new JpxImage();
|
|
157
185
|
jpx.parse(imageBuffer);
|
|
158
186
|
const tile = jpx.tiles[0];
|
|
159
|
-
|
|
187
|
+
debugLog('EID', `[EID] JP2 decoded: ${tile.width}x${tile.height}, ${jpx.componentsCount} channels, ${tile.items.length} bytes`);
|
|
160
188
|
const pngBytes = pixelsToPng(tile.items, tile.width, tile.height, jpx.componentsCount);
|
|
161
189
|
const pngBase64 = Buffer.from(pngBytes).toString('base64');
|
|
162
|
-
|
|
190
|
+
debugLog('EID', `[EID] Converted JP2 → PNG (${pngBytes.length} bytes, ${pngBase64.length} base64 chars)`);
|
|
163
191
|
return {
|
|
164
192
|
base64: pngBase64,
|
|
165
193
|
mimeType: 'image/png'
|
|
166
194
|
};
|
|
167
195
|
} catch (err) {
|
|
168
|
-
|
|
196
|
+
debugLog('EID', '[EID] JP2 conversion failed:', err);
|
|
169
197
|
return {
|
|
170
198
|
base64: imageBuffer.toString('base64'),
|
|
171
199
|
mimeType
|
|
@@ -274,8 +302,40 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
|
|
|
274
302
|
let progress = 0;
|
|
275
303
|
const nfcManagerCardService = new NFCManagerCardService();
|
|
276
304
|
const passportService = new EIDService(nfcManagerCardService, EIDService.NORMAL_MAX_TRANSCEIVE_LENGTH, EIDService.DEFAULT_MAX_BLOCKSIZE, false, true);
|
|
305
|
+
|
|
306
|
+
// Structured NFC diagnostics: record each step's outcome + timing + APDU SW
|
|
307
|
+
// into the session collector so the result screen can surface device-specific
|
|
308
|
+
// read failures. Best-effort and side-effect-only — never alters the flow.
|
|
309
|
+
diagnostics.nfcAttemptStarted();
|
|
310
|
+
const stepTimer = () => {
|
|
311
|
+
const t0 = Date.now();
|
|
312
|
+
return (step, ok, error) => {
|
|
313
|
+
const msg = error instanceof Error ? error.message : error ? String(error) : undefined;
|
|
314
|
+
// Store only the APDU status word and a COARSE category — never the raw
|
|
315
|
+
// error text, which could in theory embed field/PII values. The SW + step
|
|
316
|
+
// name + category carry all the actionable signal.
|
|
317
|
+
diagnostics.nfcStep({
|
|
318
|
+
step,
|
|
319
|
+
ok,
|
|
320
|
+
ms: Date.now() - t0,
|
|
321
|
+
sw: msg ? extractStatusWord(msg) : undefined,
|
|
322
|
+
error: msg ? categorizeNfcError(msg) : undefined
|
|
323
|
+
});
|
|
324
|
+
};
|
|
325
|
+
};
|
|
277
326
|
try {
|
|
278
|
-
|
|
327
|
+
{
|
|
328
|
+
const done = stepTimer();
|
|
329
|
+
try {
|
|
330
|
+
await passportService.open();
|
|
331
|
+
// Reaching open() without throwing means the chip powered on / was seen.
|
|
332
|
+
diagnostics.nfcChipDetected();
|
|
333
|
+
done('open', true);
|
|
334
|
+
} catch (e) {
|
|
335
|
+
done('open', false, e);
|
|
336
|
+
throw e;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
279
339
|
progress = 1;
|
|
280
340
|
if (progressCallback) {
|
|
281
341
|
progressCallback(progress);
|
|
@@ -285,12 +345,17 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
|
|
|
285
345
|
// Explicitly SELECT MF first so the SFI=0x1C context is correct,
|
|
286
346
|
// regardless of which application the chip activates on power-on.
|
|
287
347
|
let hasPACESucceeded = false;
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
348
|
+
{
|
|
349
|
+
const done = stepTimer();
|
|
350
|
+
try {
|
|
351
|
+
await passportService.sendSelectMF();
|
|
352
|
+
done('selectMF', true);
|
|
353
|
+
debugLog('EID', '[EID] SELECT MF OK');
|
|
354
|
+
} catch (mfErr) {
|
|
355
|
+
done('selectMF', false, mfErr);
|
|
356
|
+
const mfMsg = mfErr instanceof Error ? mfErr.message : String(mfErr);
|
|
357
|
+
debugLog('EID', `[EID] SELECT MF failed (${mfMsg}), continuing with SFI read anyway`);
|
|
358
|
+
}
|
|
294
359
|
}
|
|
295
360
|
progress = 2;
|
|
296
361
|
if (progressCallback) {
|
|
@@ -300,19 +365,24 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
|
|
|
300
365
|
// Read EF.CardAccess — kept in a separate try/catch so we can distinguish
|
|
301
366
|
// "file read failure" from "PACE protocol failure" in the logs.
|
|
302
367
|
let cardAccessBuf = null;
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
buf
|
|
368
|
+
{
|
|
369
|
+
const done = stepTimer();
|
|
370
|
+
try {
|
|
371
|
+
const cardAccessStream = passportService.getInputStream(EIDService.EF_CARD_ACCESS);
|
|
372
|
+
await cardAccessStream.init();
|
|
373
|
+
const cardAccessLen = cardAccessStream.getLength();
|
|
374
|
+
const buf = Buffer.alloc(cardAccessLen);
|
|
375
|
+
for (let i = 0; i < cardAccessLen; i++) {
|
|
376
|
+
buf[i] = await cardAccessStream.read();
|
|
377
|
+
}
|
|
378
|
+
cardAccessBuf = buf;
|
|
379
|
+
done('readCardAccess', true);
|
|
380
|
+
debugLog('EID', `[EID] EF.CardAccess read OK: ${cardAccessLen} bytes = ${cardAccessBuf.toString('hex')}`);
|
|
381
|
+
} catch (readErr) {
|
|
382
|
+
done('readCardAccess', false, readErr);
|
|
383
|
+
const readMsg = readErr instanceof Error ? readErr.message : String(readErr);
|
|
384
|
+
debugLog('EID', `[EID] EF.CardAccess read FAILED: ${readMsg} — falling back to BAC`);
|
|
310
385
|
}
|
|
311
|
-
cardAccessBuf = buf;
|
|
312
|
-
console.debug(`[EID] EF.CardAccess read OK: ${cardAccessLen} bytes = ${cardAccessBuf.toString('hex')}`);
|
|
313
|
-
} catch (readErr) {
|
|
314
|
-
const readMsg = readErr instanceof Error ? readErr.message : String(readErr);
|
|
315
|
-
console.debug(`[EID] EF.CardAccess read FAILED: ${readMsg} — falling back to BAC`);
|
|
316
386
|
}
|
|
317
387
|
progress = 3;
|
|
318
388
|
if (progressCallback) {
|
|
@@ -320,11 +390,17 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
|
|
|
320
390
|
}
|
|
321
391
|
|
|
322
392
|
// If EF.CardAccess was read successfully, attempt PACE.
|
|
393
|
+
if (cardAccessBuf === null) {
|
|
394
|
+
// EF.CardAccess unreadable (the readCardAccess step already recorded the
|
|
395
|
+
// failure/SW) → PACE can't be attempted; BAC will be used.
|
|
396
|
+
diagnostics.nfcPaceFellBackToBac('no_card_access');
|
|
397
|
+
}
|
|
323
398
|
if (cardAccessBuf !== null) {
|
|
324
399
|
try {
|
|
325
400
|
const paceInfo = parsePACEInfoFromCardAccess(cardAccessBuf);
|
|
326
401
|
if (paceInfo) {
|
|
327
|
-
|
|
402
|
+
debugLog('EID', `[EID] PACE available: oid=${paceInfo.oid} parameterId=${paceInfo.parameterId}`);
|
|
403
|
+
diagnostics.nfcSetAuth('PACE', paceInfo.oid);
|
|
328
404
|
// PACE is available - derive key from MRZ and perform PACE
|
|
329
405
|
const bacKey = new BACKey(documentNumber, dateOfBirth, dateOfExpiry);
|
|
330
406
|
const paceKey = PACEKeySpec.createMRZKey(bacKey.getKeySeedForPACE());
|
|
@@ -332,18 +408,27 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
|
|
|
332
408
|
if (progressCallback) {
|
|
333
409
|
progressCallback(progress);
|
|
334
410
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
411
|
+
const done = stepTimer();
|
|
412
|
+
try {
|
|
413
|
+
await passportService.doPACE(paceKey, paceInfo.oid, paceInfo.parameterId, progressCallback);
|
|
414
|
+
hasPACESucceeded = true;
|
|
415
|
+
done('paceAuth', true);
|
|
416
|
+
debugLog('EID', '[EID] PACE succeeded');
|
|
417
|
+
} catch (paceErr) {
|
|
418
|
+
done('paceAuth', false, paceErr);
|
|
419
|
+
throw paceErr;
|
|
420
|
+
}
|
|
338
421
|
} else {
|
|
339
|
-
|
|
422
|
+
diagnostics.nfcPaceFellBackToBac('no_pace_info');
|
|
423
|
+
debugLog('EID', '[EID] EF.CardAccess read OK but no PACE SecurityInfo found — document does not support PACE, falling back to BAC');
|
|
340
424
|
}
|
|
341
425
|
} catch (paceError) {
|
|
342
426
|
// PACE protocol exchange failed — fall back to BAC
|
|
427
|
+
diagnostics.nfcPaceFellBackToBac('pace_failed');
|
|
343
428
|
const msg = paceError instanceof Error ? paceError.message : String(paceError);
|
|
344
|
-
|
|
429
|
+
debugLog('EID', `[EID] PACE protocol failed (${msg}), falling back to BAC`);
|
|
345
430
|
if (paceError instanceof Error && paceError.stack) {
|
|
346
|
-
|
|
431
|
+
debugLog('EID', '[EID] PACE error stack:', paceError.stack);
|
|
347
432
|
}
|
|
348
433
|
}
|
|
349
434
|
}
|
|
@@ -353,7 +438,16 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
|
|
|
353
438
|
}
|
|
354
439
|
|
|
355
440
|
// Select Applet for MRTD
|
|
356
|
-
|
|
441
|
+
{
|
|
442
|
+
const done = stepTimer();
|
|
443
|
+
try {
|
|
444
|
+
await passportService.sendSelectApplet(hasPACESucceeded);
|
|
445
|
+
done('selectApplet', true);
|
|
446
|
+
} catch (e) {
|
|
447
|
+
done('selectApplet', false, e);
|
|
448
|
+
throw e;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
357
451
|
progress = 10;
|
|
358
452
|
if (progressCallback) {
|
|
359
453
|
progressCallback(progress);
|
|
@@ -366,8 +460,16 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
|
|
|
366
460
|
await efComStream.read();
|
|
367
461
|
} catch (error) {
|
|
368
462
|
// EF_COM not available -> try to do BAC
|
|
463
|
+
diagnostics.nfcSetAuth('BAC');
|
|
369
464
|
const bacKey = new BACKey(documentNumber, dateOfBirth, dateOfExpiry);
|
|
370
|
-
|
|
465
|
+
const done = stepTimer();
|
|
466
|
+
try {
|
|
467
|
+
await passportService.doBAC(bacKey);
|
|
468
|
+
done('bacAuth', true);
|
|
469
|
+
} catch (e) {
|
|
470
|
+
done('bacAuth', false, e);
|
|
471
|
+
throw e;
|
|
472
|
+
}
|
|
371
473
|
}
|
|
372
474
|
}
|
|
373
475
|
progress = 20;
|
|
@@ -376,16 +478,25 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
|
|
|
376
478
|
}
|
|
377
479
|
|
|
378
480
|
// Read DG1 (MRZ Info)
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
481
|
+
const dg1Done = stepTimer();
|
|
482
|
+
let mrzInfo;
|
|
483
|
+
try {
|
|
484
|
+
const dg1InputStream = passportService.getInputStream(EIDService.EF_DG1);
|
|
485
|
+
await dg1InputStream.init();
|
|
486
|
+
const dg1File = new DG1File(undefined, dg1InputStream);
|
|
487
|
+
mrzInfo = await dg1File.getMRZInfo();
|
|
488
|
+
dg1Done('readDG1', true);
|
|
489
|
+
} catch (e) {
|
|
490
|
+
dg1Done('readDG1', false, e);
|
|
491
|
+
throw e;
|
|
492
|
+
}
|
|
383
493
|
progress = 30;
|
|
384
494
|
if (progressCallback) {
|
|
385
495
|
progressCallback(progress);
|
|
386
496
|
}
|
|
387
497
|
|
|
388
498
|
// Read DG2 (Face Image)
|
|
499
|
+
const dg2Done = stepTimer();
|
|
389
500
|
const dg2InputStream = passportService.getInputStream(EIDService.EF_DG2);
|
|
390
501
|
await dg2InputStream.init();
|
|
391
502
|
InputStream.onRead((totalBytesRead, fileLength) => {
|
|
@@ -398,7 +509,14 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
|
|
|
398
509
|
let imageAsBase64 = '';
|
|
399
510
|
let mimeType = '';
|
|
400
511
|
const allFaceImageInfos = [];
|
|
401
|
-
|
|
512
|
+
let faceInfos;
|
|
513
|
+
try {
|
|
514
|
+
faceInfos = await dg2File.getFaceInfos();
|
|
515
|
+
dg2Done('readDG2', true);
|
|
516
|
+
} catch (e) {
|
|
517
|
+
dg2Done('readDG2', false, e);
|
|
518
|
+
throw e;
|
|
519
|
+
}
|
|
402
520
|
for (let faceInfo of faceInfos) {
|
|
403
521
|
allFaceImageInfos.push(...faceInfo.getFaceImageInfos());
|
|
404
522
|
}
|
|
@@ -417,13 +535,16 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
|
|
|
417
535
|
if (progressCallback) {
|
|
418
536
|
progressCallback(progress);
|
|
419
537
|
}
|
|
538
|
+
diagnostics.nfcResult(true);
|
|
420
539
|
return {
|
|
421
540
|
mrz: mrzInfo,
|
|
422
541
|
faceImage: imageAsBase64,
|
|
423
542
|
mimeType: mimeType
|
|
424
543
|
};
|
|
425
544
|
} catch (error) {
|
|
426
|
-
|
|
545
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
546
|
+
diagnostics.nfcResult(false, categorizeNfcError(msg));
|
|
547
|
+
debugLog('EID', 'Error reading passport data', error);
|
|
427
548
|
} finally {
|
|
428
549
|
await passportService.close();
|
|
429
550
|
}
|
|
@@ -56,6 +56,40 @@ primary safeguard. A single noisy frame is not trusted; the consensus is.
|
|
|
56
56
|
|
|
57
57
|
---
|
|
58
58
|
|
|
59
|
+
## 2a. Turkish e-ID document numbers: leading serial LETTER must be preserved
|
|
60
|
+
|
|
61
|
+
**Issue.** Turkish identity cards put a **serial letter** at the start of the
|
|
62
|
+
9-char document number (e.g. `A53032352`, `A49M38410`). Empirically — verified
|
|
63
|
+
against real cards — **Türkiye computes the document-number check digit over the
|
|
64
|
+
number with the LEADING letter EXCLUDED** (interior letters are kept), which
|
|
65
|
+
diverges from strict ICAO 9303 (where `A`=10 is weighted in). So a genuine card
|
|
66
|
+
`A53032352` (printed check digit `5`) **fails** the strict ICAO check (which
|
|
67
|
+
wants `7`), even though the card is valid — `icaoCheckDigit("53032352") === "5"`.
|
|
68
|
+
The TD1 **composite** on these cards is likewise non-standard (no interpretation
|
|
69
|
+
of the leading letter reproduces it), so it is **not** a reliable anchor.
|
|
70
|
+
|
|
71
|
+
**Why it matters.** The leading letter is **real** and **must be preserved**: the
|
|
72
|
+
**NFC eID chip derives its BAC/PACE access key from the document number exactly
|
|
73
|
+
as stored** (with the letter). Rewriting `A53032352`→`453032352` — as the
|
|
74
|
+
ambiguous-char recovery would, to satisfy strict ICAO — makes the **chip read
|
|
75
|
+
FAIL**. (The OCR-B `4` glyph closely resembles `A`, which is why the recovery
|
|
76
|
+
used to "correct" it.)
|
|
77
|
+
|
|
78
|
+
**Mitigation.** `acceptsTurkishLetterPrefix` (in `mrz.utils.ts`; ported to
|
|
79
|
+
`mrz_utils.dart`) accepts a TUR TD1 whose only failing checks are
|
|
80
|
+
document-number/composite **when** the document number starts with a letter
|
|
81
|
+
**and** `icaoCheckDigit(documentNumber.slice(1))` matches the printed
|
|
82
|
+
document-number check digit. The parse is then accepted **as-is**, preserving the
|
|
83
|
+
letter; the composite is deliberately not required. The subsequent **NFC read is
|
|
84
|
+
the authoritative confirmation** — it only succeeds with the correct
|
|
85
|
+
(letter-preserving) number. Contrast `A49M38410`, whose letters validate under
|
|
86
|
+
*standard* ICAO and so are kept by the normal path.
|
|
87
|
+
|
|
88
|
+
Covered by the "Turkish e-ID … leading serial LETTER" tests in
|
|
89
|
+
`mrz.ambiguous.test.ts` (RN) and `mrz_ambiguous_test.dart` (Flutter).
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
59
93
|
## 3. Filler-letter normalisation is conservative in name fields (by design)
|
|
60
94
|
|
|
61
95
|
**Issue.** The `<` filler is frequently OCR'd as a letter (`K`, and under glare
|
|
@@ -84,6 +118,13 @@ Covered by the "real letters in names are not eaten" tests in `mrz.utils.test.ts
|
|
|
84
118
|
30–44-char MRZ line below that floor, so the native pipeline **crops to the
|
|
85
119
|
on-screen scan frame and upscales + grayscale/contrast-enhances** before OCR.
|
|
86
120
|
The MRZ band must be reasonably filled within the on-screen guide.
|
|
121
|
+
- Two distinct regions exist in the native code (don't confuse them):
|
|
122
|
+
the **MRZ-OCR crop** (`buildScanFrameRoiImage`: top 25% / bottom 12% / sides
|
|
123
|
+
4%, upscaled ≤2.5× to ≤2200px, grayscale, contrast 1.6×) and the
|
|
124
|
+
**brightness-sampling region** (the on-screen guide: 36% / 36% / 5%). Both are
|
|
125
|
+
the same on iOS and Android. The diagnostic report records the measured MRZ
|
|
126
|
+
band pixel height so you can tell when a device leaves the band below the
|
|
127
|
+
~16 px/char floor.
|
|
87
128
|
- **Glare / lamination.** Heavy glare on a glossy laminated card still degrades
|
|
88
129
|
OCR; the enhancement helps but does not eliminate it. Steadying the card and
|
|
89
130
|
reducing direct glare improves convergence.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import EventSource from 'react-native-sse';
|
|
4
|
+
import { debugLog } from "./debug.utils.js";
|
|
4
5
|
export class SignalingClient {
|
|
5
6
|
eventSource = null;
|
|
6
7
|
constructor(baseUrl, sessionId, onMessage, identificationId, onSessionEnded) {
|
|
@@ -19,13 +20,13 @@ export class SignalingClient {
|
|
|
19
20
|
urlParams.append('identificationId', this.identificationId);
|
|
20
21
|
}
|
|
21
22
|
const url = `${this.baseUrl}/api/app/mobile/video-sessions/${this.sessionId}/signaling/stream?${urlParams.toString()}`;
|
|
22
|
-
|
|
23
|
+
debugLog('SignalingClient', '[SignalingClient] Connecting to SSE:', url);
|
|
23
24
|
this.eventSource = new EventSource(url, {
|
|
24
25
|
pollingInterval: 0 // 0 means default/no polling if SSE is real
|
|
25
26
|
});
|
|
26
27
|
const listener = event => {
|
|
27
28
|
if (event.type === 'open') {
|
|
28
|
-
|
|
29
|
+
debugLog('SignalingClient', '[SignalingClient] Connected');
|
|
29
30
|
this.onConnected?.();
|
|
30
31
|
} else if (event.type === 'error') {
|
|
31
32
|
console.error('[SignalingClient] Connection error:', JSON.stringify(event, null, 2));
|
|
@@ -64,8 +65,8 @@ export class SignalingClient {
|
|
|
64
65
|
};
|
|
65
66
|
|
|
66
67
|
// Ping handler - server sends pings to keep connection alive
|
|
67
|
-
const pingListener =
|
|
68
|
-
|
|
68
|
+
const pingListener = _event => {
|
|
69
|
+
debugLog('SignalingClient', '[SignalingClient] Received ping');
|
|
69
70
|
// Just acknowledge by doing nothing, server-side heartbeat keeps connection alive
|
|
70
71
|
};
|
|
71
72
|
|
|
@@ -73,7 +74,7 @@ export class SignalingClient {
|
|
|
73
74
|
const sessionEndedListener = event => {
|
|
74
75
|
try {
|
|
75
76
|
const data = JSON.parse(event.data || '{}');
|
|
76
|
-
|
|
77
|
+
debugLog('SignalingClient', '[SignalingClient] Session ended:', data.state);
|
|
77
78
|
if (this.onSessionEnded) {
|
|
78
79
|
this.onSessionEnded(data.state);
|
|
79
80
|
}
|
|
@@ -5,19 +5,41 @@ import { Buffer } from 'buffer';
|
|
|
5
5
|
import { gcm } from '@noble/ciphers/aes';
|
|
6
6
|
import { randomBytes } from '@noble/ciphers/webcrypto';
|
|
7
7
|
import { secp256k1 } from '@noble/curves/secp256k1';
|
|
8
|
-
import
|
|
8
|
+
import { hkdf } from '@noble/hashes/hkdf';
|
|
9
|
+
import { sha256 } from '@noble/hashes/sha2';
|
|
10
|
+
import httpClient, { setSessionToken } from "./http-client.js";
|
|
11
|
+
|
|
12
|
+
// Must stay byte-for-byte identical to the backend derivation
|
|
13
|
+
// (web-app/src/lib/crypto-utils.ts).
|
|
14
|
+
const SESSION_KEY_KDF_INFO = 'trustchex-session-key-v2';
|
|
15
|
+
const SESSION_KEY_KDF_VERSION = 2;
|
|
9
16
|
const getSessionKey = async (apiUrl, sessionId) => {
|
|
10
17
|
const clientPrivateKey = secp256k1.utils.randomPrivateKey();
|
|
11
18
|
const clientPublicKey = secp256k1.getPublicKey(clientPrivateKey);
|
|
12
19
|
const serverResponse = await httpClient.post(`${apiUrl}/verification-sessions/${sessionId}/auth`, {
|
|
13
|
-
clientPublicKey: Buffer.from(clientPublicKey).toString('hex')
|
|
20
|
+
clientPublicKey: Buffer.from(clientPublicKey).toString('hex'),
|
|
21
|
+
kdfVersion: SESSION_KEY_KDF_VERSION
|
|
14
22
|
});
|
|
23
|
+
if (serverResponse.sessionToken) {
|
|
24
|
+
setSessionToken(serverResponse.sessionToken);
|
|
25
|
+
}
|
|
15
26
|
const sharedKey = secp256k1.getSharedSecret(clientPrivateKey, Buffer.from(serverResponse.serverPublicKey, 'hex').toString('hex'));
|
|
27
|
+
|
|
28
|
+
// The server echoes the kdfVersion it actually used; older backends don't
|
|
29
|
+
// send the field and derive the legacy key.
|
|
30
|
+
if (serverResponse.kdfVersion === SESSION_KEY_KDF_VERSION) {
|
|
31
|
+
// sharedKey is a compressed secp256k1 point; byte 0 is the 0x02/0x03
|
|
32
|
+
// parity prefix and carries no secret entropy.
|
|
33
|
+
const derived = hkdf(sha256, Buffer.from(sharedKey).subarray(1), Buffer.from(sessionId, 'utf-8'), SESSION_KEY_KDF_INFO, 32);
|
|
34
|
+
return Buffer.from(derived).toString('hex');
|
|
35
|
+
}
|
|
16
36
|
const sessionKey = (Buffer.from(new Date().getUTCDate().toString(), 'utf-8').toString('hex') + Buffer.from(sharedKey).toString('hex')).substring(0, 32);
|
|
17
37
|
return sessionKey;
|
|
18
38
|
};
|
|
19
39
|
const encryptWithAes = (data, key) => {
|
|
20
|
-
|
|
40
|
+
// Standard 96-bit GCM IV; the backend reads the nonce from the payload, so
|
|
41
|
+
// this stays compatible with servers that previously saw 32-byte nonces.
|
|
42
|
+
const nonce = randomBytes(12);
|
|
21
43
|
const dataBytes = Buffer.from(data, 'utf-8');
|
|
22
44
|
const aes = gcm(Buffer.from(key, 'hex'), nonce);
|
|
23
45
|
const ciphertext = aes.encrypt(dataBytes);
|
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { debugLog } from "./debug.utils.js";
|
|
3
|
+
import { debugLog, logWarn } from "./debug.utils.js";
|
|
4
|
+
|
|
5
|
+
// Hostname (optionally with port) — rejects paths, credentials, and other
|
|
6
|
+
// URL components smuggled into the app-url segment.
|
|
7
|
+
const HOST_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9.-]*(:\d{1,5})?$/;
|
|
8
|
+
const getUrlScheme = url => {
|
|
9
|
+
const match = url.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\//);
|
|
10
|
+
return match ? match[1].toLowerCase() : '';
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// The SDK uploads identity documents and biometrics to the derived baseUrl, so
|
|
14
|
+
// cleartext destinations are only allowed in development builds.
|
|
15
|
+
const isAllowedScheme = scheme => {
|
|
16
|
+
return scheme === 'https' || __DEV__ && scheme === 'http';
|
|
17
|
+
};
|
|
18
|
+
export const getUrlHost = url => {
|
|
19
|
+
const withoutScheme = url.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, '');
|
|
20
|
+
return (withoutScheme.split('/')[0] ?? '').toLowerCase();
|
|
21
|
+
};
|
|
4
22
|
const handleDeepLink = ({
|
|
5
23
|
url
|
|
6
24
|
}) => {
|
|
7
25
|
debugLog('handleDeepLink', 'Received URL:', url);
|
|
8
|
-
const
|
|
9
|
-
const route = url.
|
|
26
|
+
const scheme = getUrlScheme(url);
|
|
27
|
+
const route = scheme ? url.slice(`${scheme}://`.length) : url;
|
|
10
28
|
const segments = route.split('/');
|
|
11
29
|
debugLog('handleDeepLink', 'Parsed segments:', segments);
|
|
12
30
|
let baseUrl = '';
|
|
@@ -16,16 +34,28 @@ const handleDeepLink = ({
|
|
|
16
34
|
sessionId = segments[i + 1] ?? '';
|
|
17
35
|
debugLog('handleDeepLink', 'Found sessionId:', sessionId);
|
|
18
36
|
} else if (segments[i] === 'app-url') {
|
|
19
|
-
|
|
37
|
+
const host = segments[i + 1] ?? '';
|
|
38
|
+
if (!HOST_PATTERN.test(host)) {
|
|
39
|
+
logWarn('handleDeepLink', 'Rejected invalid app-url host');
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const hostScheme = scheme === 'http' ? 'http' : 'https';
|
|
43
|
+
if (!isAllowedScheme(hostScheme)) {
|
|
44
|
+
logWarn('handleDeepLink', 'Rejected non-HTTPS app-url');
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
baseUrl = `${hostScheme}://${host}`;
|
|
20
48
|
debugLog('handleDeepLink', 'Found baseUrl:', baseUrl);
|
|
21
49
|
}
|
|
22
50
|
}
|
|
23
51
|
|
|
24
52
|
// If no app-url segment found, derive baseUrl from the URL itself
|
|
25
53
|
if (!baseUrl && sessionId) {
|
|
26
|
-
const match = url.match(/^(https
|
|
27
|
-
if (match) {
|
|
28
|
-
baseUrl = match[1]
|
|
54
|
+
const match = url.match(/^(https?):\/\/([^/]+)/);
|
|
55
|
+
if (match && isAllowedScheme(match[1].toLowerCase())) {
|
|
56
|
+
baseUrl = `${match[1].toLowerCase()}://${match[2]}`;
|
|
57
|
+
} else if (match) {
|
|
58
|
+
logWarn('handleDeepLink', 'Rejected non-HTTPS deep link URL');
|
|
29
59
|
}
|
|
30
60
|
}
|
|
31
61
|
debugLog('handleDeepLink', 'Returning:', {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builds the support diagnostic report from a scan session.
|
|
5
|
+
*
|
|
6
|
+
* Produces three things the result screen turns into a reviewable email:
|
|
7
|
+
* - `diagnostics.json` — technical signals (device, camera, MRZ, NFC steps).
|
|
8
|
+
* - `scan-data.json` — the scanned identity data + raw MRZ (PII; the user
|
|
9
|
+
* reviews it in the draft and chooses to send).
|
|
10
|
+
* - a short, size-bounded plain-text BODY summary that always fits an email.
|
|
11
|
+
*
|
|
12
|
+
* The full detail lives in the JSON attachments; the email body is intentionally
|
|
13
|
+
* compact so the draft opens reliably on every mail client.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** ICAO 9303 7-3-1 weighted check digit (local copy; mirrors mrz.utils). */
|
|
17
|
+
const icaoCheckDigit = str => {
|
|
18
|
+
const weights = [7, 3, 1];
|
|
19
|
+
let sum = 0;
|
|
20
|
+
for (let i = 0; i < str.length; i++) {
|
|
21
|
+
const c = str[i];
|
|
22
|
+
const v = c === '<' ? 0 : c >= '0' && c <= '9' ? c.charCodeAt(0) - 48 : c.charCodeAt(0) - 55;
|
|
23
|
+
sum = (sum + weights[i % 3] * v) % 10;
|
|
24
|
+
}
|
|
25
|
+
return String(sum);
|
|
26
|
+
};
|
|
27
|
+
/** Compute per-field check-digit pass/fail for a TD1 MRZ (best-effort). */
|
|
28
|
+
const computeTd1CheckDigits = mrzText => {
|
|
29
|
+
if (!mrzText) return undefined;
|
|
30
|
+
const lines = mrzText.split('\n').map(l => l.trim()).filter(Boolean);
|
|
31
|
+
if (lines.length < 2 || lines[0].length < 30 || lines[1].length < 30) {
|
|
32
|
+
return undefined; // only TD1 handled here; other formats omitted from summary
|
|
33
|
+
}
|
|
34
|
+
const l1 = lines[0];
|
|
35
|
+
const l2 = lines[1];
|
|
36
|
+
const result = {};
|
|
37
|
+
const check = (data, printed) => icaoCheckDigit(data) === printed ? 'pass' : 'fail';
|
|
38
|
+
|
|
39
|
+
// Turkish convention: a doc number may carry a leading serial LETTER, and the
|
|
40
|
+
// doc-number check digit is computed over the DIGITS ONLY (letter dropped).
|
|
41
|
+
// The TD1 composite then also diverges from strict ICAO for such cards (no
|
|
42
|
+
// interpretation of the letter reproduces it). To avoid reporting a misleading
|
|
43
|
+
// "fail" on a card the SDK legitimately accepted, detect the letter-prefix and
|
|
44
|
+
// treat both the doc-number and composite as validated under that convention.
|
|
45
|
+
const docData = l1.slice(5, 14);
|
|
46
|
+
const docPrinted = l1[14];
|
|
47
|
+
const hasLetterPrefix = /^[A-Z]/.test(docData);
|
|
48
|
+
const strictDocOk = icaoCheckDigit(docData) === docPrinted;
|
|
49
|
+
const turkishDocOk = hasLetterPrefix && icaoCheckDigit(docData.slice(1)) === docPrinted;
|
|
50
|
+
const docOk = strictDocOk || turkishDocOk;
|
|
51
|
+
result.documentNumber = docOk ? 'pass' : 'fail';
|
|
52
|
+
result.birthDate = check(l2.slice(0, 6), l2[6]);
|
|
53
|
+
result.expiry = check(l2.slice(8, 14), l2[14]);
|
|
54
|
+
const composite = l1.slice(5, 30) + l2.slice(0, 7) + l2.slice(8, 15) + l2.slice(18, 29);
|
|
55
|
+
const compStrict = check(composite, l2[29]);
|
|
56
|
+
// If the strict composite fails ONLY because of the Turkish letter-prefix
|
|
57
|
+
// doc number (which itself passed under the digits-only rule), don't surface a
|
|
58
|
+
// misleading failure — the SDK accepts these cards.
|
|
59
|
+
result.composite = compStrict === 'fail' && hasLetterPrefix && docOk ? 'pass' : compStrict;
|
|
60
|
+
// The Turkish convention was the deciding factor only when strict ICAO would
|
|
61
|
+
// have failed the doc number but the digits-only rule passes it.
|
|
62
|
+
const turkishLetterPrefix = !strictDocOk && turkishDocOk;
|
|
63
|
+
return {
|
|
64
|
+
checkDigits: result,
|
|
65
|
+
turkishLetterPrefix
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
const MAX_BODY_BYTES = 4096;
|
|
69
|
+
const yesNo = b => b ? 'yes' : 'no';
|
|
70
|
+
|
|
71
|
+
/** Assemble the full report bundle. Pure — no I/O, no native calls. */
|
|
72
|
+
export const buildDiagnosticReport = params => {
|
|
73
|
+
const {
|
|
74
|
+
diag,
|
|
75
|
+
device,
|
|
76
|
+
scan,
|
|
77
|
+
sdkVersion,
|
|
78
|
+
sessionId
|
|
79
|
+
} = params;
|
|
80
|
+
const computed = computeTd1CheckDigits(scan.mrzText);
|
|
81
|
+
const checkDigits = diag.mrz.checkDigits ?? computed?.checkDigits;
|
|
82
|
+
// Set the Turkish-letter-prefix flag from the final accepted MRZ (the collector
|
|
83
|
+
// can't know it during scanning; the report does, from the MRZ text).
|
|
84
|
+
const turkishLetterPrefixApplied = diag.mrz.turkishLetterPrefixApplied || computed?.turkishLetterPrefix === true;
|
|
85
|
+
|
|
86
|
+
// --- diagnostics.json (technical, low-PII) ---
|
|
87
|
+
const diagnosticsObj = {
|
|
88
|
+
schemaVersion: diag.schemaVersion,
|
|
89
|
+
generatedAt: diag.generatedAt,
|
|
90
|
+
sessionId: sessionId ?? null,
|
|
91
|
+
sdk: {
|
|
92
|
+
platform: device.platform,
|
|
93
|
+
sdkVersion,
|
|
94
|
+
buildNumber: device.buildNumber
|
|
95
|
+
},
|
|
96
|
+
device,
|
|
97
|
+
scan: {
|
|
98
|
+
documentType: scan.documentType ?? diag.documentType ?? null,
|
|
99
|
+
dataSource: scan.dataSource ?? diag.dataSource ?? null,
|
|
100
|
+
totalDurationMs: diag.totalDurationMs,
|
|
101
|
+
camera: diag.camera
|
|
102
|
+
},
|
|
103
|
+
mrz: {
|
|
104
|
+
...diag.mrz,
|
|
105
|
+
checkDigits: checkDigits ?? null,
|
|
106
|
+
turkishLetterPrefixApplied
|
|
107
|
+
},
|
|
108
|
+
nfc: diag.nfc,
|
|
109
|
+
errors: diag.errors
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// --- scan-data.json (PII; user reviews before sending) ---
|
|
113
|
+
const scanDataObj = {
|
|
114
|
+
documentType: scan.documentType ?? diag.documentType ?? null,
|
|
115
|
+
dataSource: scan.dataSource ?? diag.dataSource ?? null,
|
|
116
|
+
mrzText: scan.mrzText ?? null,
|
|
117
|
+
fields: scan.mrzFields ?? null,
|
|
118
|
+
barcodeValue: scan.barcodeValue ?? null
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// --- compact email body (always under MAX_BODY_BYTES) ---
|
|
122
|
+
const nfc = diag.nfc;
|
|
123
|
+
const nfcLine = nfc.attempted ? `${nfc.authProtocol}${nfc.paceFellBackToBac ? '→BAC' : ''} · ${nfc.succeeded ? 'success' : `failed at ${nfc.failureStep ?? '?'} (${nfc.errorCategory ?? 'error'})`}` : 'not attempted';
|
|
124
|
+
const checksLine = checkDigits ? Object.entries(checkDigits).map(([k, v]) => `${k}:${v}`).join(' ') : 'n/a';
|
|
125
|
+
const body = ['TRUSTCHEX SCAN REPORT', `Device: ${device.brand} ${device.model} · ${device.systemName} ${device.systemVersion}${device.apiLevel ? ` (API ${device.apiLevel})` : ''}`, `SDK: ${sdkVersion} (build ${device.buildNumber}) · ${device.manufacturer} · ${device.deviceId}`, `Document: ${scanDataObj.documentType ?? 'unknown'} · source: ${scanDataObj.dataSource ?? 'unknown'}`, `MRZ: ${diag.mrz.reachedStable ? 'stable' : 'NOT stable'} after ${diag.mrz.framesProcessed} frames · checks: ${checksLine}`, `Camera: ${diag.camera.frames} frames · brightness ${diag.camera.avgBrightness} (low:${yesNo(diag.camera.hadLowBrightness)} blur:${yesNo(diag.camera.hadBlurryFrames)})`, `NFC: ${nfcLine}`, sessionId ? `Session: ${sessionId}` : '', '', 'Describe the issue here:', '', '', '— Full diagnostics and document images are attached. Please review before sending; do not edit the attachments. —'].filter(Boolean).join('\n');
|
|
126
|
+
const subject = `Trustchex scan report — ${scanDataObj.documentType ?? 'doc'} — ${device.brand} ${device.model} — v${sdkVersion}${sessionId ? ` — ${sessionId}` : ''}`;
|
|
127
|
+
return {
|
|
128
|
+
diagnosticsJson: JSON.stringify(diagnosticsObj, null, 2),
|
|
129
|
+
scanDataJson: JSON.stringify(scanDataObj, null, 2),
|
|
130
|
+
body: body.length > MAX_BODY_BYTES ? body.slice(0, MAX_BODY_BYTES - 1) + '…' : body,
|
|
131
|
+
subject
|
|
132
|
+
};
|
|
133
|
+
};
|