@hexar/biometric-identity-sdk-core 1.0.14 → 1.0.16
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.
|
@@ -79,6 +79,24 @@ export declare class BiometricIdentitySDK {
|
|
|
79
79
|
* Convert Blob to base64
|
|
80
80
|
*/
|
|
81
81
|
private blobToBase64;
|
|
82
|
+
/**
|
|
83
|
+
* Compress base64 image - simplified version that works in all environments
|
|
84
|
+
* For React Native, images should be compressed at capture time
|
|
85
|
+
* This is a no-op for now - compression should happen at capture time in React Native
|
|
86
|
+
*/
|
|
87
|
+
private compressImage;
|
|
88
|
+
/**
|
|
89
|
+
* Compress image in browser environment using Canvas API
|
|
90
|
+
*/
|
|
91
|
+
private compressImageBrowser;
|
|
92
|
+
/**
|
|
93
|
+
* Fix base64 padding issues
|
|
94
|
+
*/
|
|
95
|
+
private fixBase64Padding;
|
|
96
|
+
/**
|
|
97
|
+
* Validate if string is valid base64
|
|
98
|
+
*/
|
|
99
|
+
private isValidBase64;
|
|
82
100
|
/**
|
|
83
101
|
* Get current SDK state
|
|
84
102
|
*/
|
|
@@ -299,7 +299,7 @@ class BiometricIdentitySDK {
|
|
|
299
299
|
minimumRequired: 10
|
|
300
300
|
});
|
|
301
301
|
}
|
|
302
|
-
const MAX_FRAMES =
|
|
302
|
+
const MAX_FRAMES = 20;
|
|
303
303
|
if (videoFrames.length > MAX_FRAMES) {
|
|
304
304
|
const step = Math.floor(videoFrames.length / MAX_FRAMES);
|
|
305
305
|
videoFrames = videoFrames.filter((_, index) => index % step === 0).slice(0, MAX_FRAMES);
|
|
@@ -308,17 +308,46 @@ class BiometricIdentitySDK {
|
|
|
308
308
|
sampledCount: videoFrames.length
|
|
309
309
|
});
|
|
310
310
|
}
|
|
311
|
+
const compressedFrontImage = this.fixBase64Padding(await this.compressImage(this.state.frontID.data));
|
|
312
|
+
const compressedBackImage = this.state.backID?.data
|
|
313
|
+
? this.fixBase64Padding(await this.compressImage(this.state.backID.data))
|
|
314
|
+
: undefined;
|
|
315
|
+
const validFrames = [];
|
|
316
|
+
for (let i = 0; i < videoFrames.length; i++) {
|
|
317
|
+
const frame = videoFrames[i];
|
|
318
|
+
if (!frame || typeof frame !== 'string') {
|
|
319
|
+
logger_1.logger.warn(`Skipping invalid frame ${i}: not a string`);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (frame.length < 100) {
|
|
323
|
+
logger_1.logger.warn(`Skipping invalid frame ${i}: too short (${frame.length} chars), likely a file path`);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (!this.isValidBase64(frame)) {
|
|
327
|
+
logger_1.logger.warn(`Skipping invalid frame ${i}: not valid base64`);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const compressedFrame = this.fixBase64Padding(await this.compressImage(frame));
|
|
331
|
+
validFrames.push(compressedFrame);
|
|
332
|
+
}
|
|
333
|
+
if (validFrames.length < 10) {
|
|
334
|
+
logger_1.logger.error(`Insufficient valid frames: ${validFrames.length} < 10`);
|
|
335
|
+
throw new Error(`Insufficient valid video frames: ${validFrames.length} frames available, minimum 10 required`);
|
|
336
|
+
}
|
|
311
337
|
logger_1.logger.info('Sending validation request to backend', {
|
|
312
|
-
framesCount:
|
|
338
|
+
framesCount: validFrames.length,
|
|
313
339
|
duration: this.state.videoData.duration,
|
|
314
340
|
challengesCount: this.state.videoData.challengesCompleted?.length || 0,
|
|
315
|
-
frontImageSize:
|
|
316
|
-
backImageSize:
|
|
341
|
+
frontImageSize: compressedFrontImage.length,
|
|
342
|
+
backImageSize: compressedBackImage?.length || 0,
|
|
343
|
+
originalFrontSize: this.state.frontID.data?.length || 0,
|
|
344
|
+
originalBackSize: this.state.backID?.data?.length || 0,
|
|
345
|
+
compressionRatio: compressedFrontImage.length / (this.state.frontID.data?.length || 1)
|
|
317
346
|
});
|
|
318
347
|
const response = await this.backendClient.fullValidation({
|
|
319
|
-
frontIdImage:
|
|
320
|
-
backIdImage:
|
|
321
|
-
videoFrames:
|
|
348
|
+
frontIdImage: compressedFrontImage,
|
|
349
|
+
backIdImage: compressedBackImage,
|
|
350
|
+
videoFrames: validFrames,
|
|
322
351
|
videoDurationMs: this.state.videoData.duration,
|
|
323
352
|
challengesCompleted: this.state.videoData.challengesCompleted || [],
|
|
324
353
|
});
|
|
@@ -387,6 +416,92 @@ class BiometricIdentitySDK {
|
|
|
387
416
|
reader.readAsDataURL(blob);
|
|
388
417
|
});
|
|
389
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* Compress base64 image - simplified version that works in all environments
|
|
421
|
+
* For React Native, images should be compressed at capture time
|
|
422
|
+
* This is a no-op for now - compression should happen at capture time in React Native
|
|
423
|
+
*/
|
|
424
|
+
async compressImage(base64Image) {
|
|
425
|
+
if (!base64Image || base64Image.length === 0) {
|
|
426
|
+
return base64Image;
|
|
427
|
+
}
|
|
428
|
+
const MAX_SIZE = 1024 * 1024;
|
|
429
|
+
if (base64Image.length < MAX_SIZE) {
|
|
430
|
+
return base64Image;
|
|
431
|
+
}
|
|
432
|
+
try {
|
|
433
|
+
const globalWindow = globalThis.window;
|
|
434
|
+
if (globalWindow && globalWindow.Image && globalWindow.document) {
|
|
435
|
+
return await this.compressImageBrowser(base64Image, globalWindow);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
logger_1.logger.warn('Image compression not available, using original', error);
|
|
440
|
+
}
|
|
441
|
+
return base64Image;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Compress image in browser environment using Canvas API
|
|
445
|
+
*/
|
|
446
|
+
async compressImageBrowser(base64Image, window) {
|
|
447
|
+
return new Promise((resolve, reject) => {
|
|
448
|
+
const img = new window.Image();
|
|
449
|
+
img.onload = () => {
|
|
450
|
+
const canvas = window.document.createElement('canvas');
|
|
451
|
+
const maxWidth = 1920;
|
|
452
|
+
const maxHeight = 1920;
|
|
453
|
+
let width = img.width;
|
|
454
|
+
let height = img.height;
|
|
455
|
+
if (width > maxWidth || height > maxHeight) {
|
|
456
|
+
const ratio = Math.min(maxWidth / width, maxHeight / height);
|
|
457
|
+
width = width * ratio;
|
|
458
|
+
height = height * ratio;
|
|
459
|
+
}
|
|
460
|
+
canvas.width = width;
|
|
461
|
+
canvas.height = height;
|
|
462
|
+
const ctx = canvas.getContext('2d');
|
|
463
|
+
if (!ctx) {
|
|
464
|
+
reject(new Error('Could not get canvas context'));
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
468
|
+
const compressedBase64 = canvas.toDataURL('image/jpeg', 0.85);
|
|
469
|
+
const base64 = compressedBase64.split(',')[1];
|
|
470
|
+
resolve(base64);
|
|
471
|
+
};
|
|
472
|
+
img.onerror = reject;
|
|
473
|
+
img.src = `data:image/jpeg;base64,${base64Image}`;
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Fix base64 padding issues
|
|
478
|
+
*/
|
|
479
|
+
fixBase64Padding(base64) {
|
|
480
|
+
if (!base64)
|
|
481
|
+
return base64;
|
|
482
|
+
let cleaned = base64.replace(/\s/g, '');
|
|
483
|
+
if (cleaned.includes(',')) {
|
|
484
|
+
cleaned = cleaned.split(',')[1] || cleaned;
|
|
485
|
+
}
|
|
486
|
+
const padding = cleaned.length % 4;
|
|
487
|
+
if (padding > 0) {
|
|
488
|
+
cleaned += '='.repeat(4 - padding);
|
|
489
|
+
}
|
|
490
|
+
return cleaned;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Validate if string is valid base64
|
|
494
|
+
*/
|
|
495
|
+
isValidBase64(str) {
|
|
496
|
+
if (!str || typeof str !== 'string')
|
|
497
|
+
return false;
|
|
498
|
+
const cleaned = str.replace(/\s/g, '');
|
|
499
|
+
if (cleaned.includes(',')) {
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
503
|
+
return base64Regex.test(cleaned) && cleaned.length > 100;
|
|
504
|
+
}
|
|
390
505
|
/**
|
|
391
506
|
* Get current SDK state
|
|
392
507
|
*/
|
package/package.json
CHANGED
|
@@ -381,7 +381,7 @@ export class BiometricIdentitySDK {
|
|
|
381
381
|
});
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
-
const MAX_FRAMES =
|
|
384
|
+
const MAX_FRAMES = 20;
|
|
385
385
|
if (videoFrames.length > MAX_FRAMES) {
|
|
386
386
|
const step = Math.floor(videoFrames.length / MAX_FRAMES);
|
|
387
387
|
videoFrames = videoFrames.filter((_, index) => index % step === 0).slice(0, MAX_FRAMES);
|
|
@@ -391,18 +391,53 @@ export class BiometricIdentitySDK {
|
|
|
391
391
|
});
|
|
392
392
|
}
|
|
393
393
|
|
|
394
|
+
const compressedFrontImage = this.fixBase64Padding(await this.compressImage(this.state.frontID.data));
|
|
395
|
+
const compressedBackImage = this.state.backID?.data
|
|
396
|
+
? this.fixBase64Padding(await this.compressImage(this.state.backID.data))
|
|
397
|
+
: undefined;
|
|
398
|
+
|
|
399
|
+
const validFrames: string[] = [];
|
|
400
|
+
for (let i = 0; i < videoFrames.length; i++) {
|
|
401
|
+
const frame = videoFrames[i];
|
|
402
|
+
if (!frame || typeof frame !== 'string') {
|
|
403
|
+
logger.warn(`Skipping invalid frame ${i}: not a string`);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (frame.length < 100) {
|
|
408
|
+
logger.warn(`Skipping invalid frame ${i}: too short (${frame.length} chars), likely a file path`);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!this.isValidBase64(frame)) {
|
|
413
|
+
logger.warn(`Skipping invalid frame ${i}: not valid base64`);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const compressedFrame = this.fixBase64Padding(await this.compressImage(frame));
|
|
418
|
+
validFrames.push(compressedFrame);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (validFrames.length < 10) {
|
|
422
|
+
logger.error(`Insufficient valid frames: ${validFrames.length} < 10`);
|
|
423
|
+
throw new Error(`Insufficient valid video frames: ${validFrames.length} frames available, minimum 10 required`);
|
|
424
|
+
}
|
|
425
|
+
|
|
394
426
|
logger.info('Sending validation request to backend', {
|
|
395
|
-
framesCount:
|
|
427
|
+
framesCount: validFrames.length,
|
|
396
428
|
duration: this.state.videoData.duration,
|
|
397
429
|
challengesCount: this.state.videoData.challengesCompleted?.length || 0,
|
|
398
|
-
frontImageSize:
|
|
399
|
-
backImageSize:
|
|
430
|
+
frontImageSize: compressedFrontImage.length,
|
|
431
|
+
backImageSize: compressedBackImage?.length || 0,
|
|
432
|
+
originalFrontSize: this.state.frontID.data?.length || 0,
|
|
433
|
+
originalBackSize: this.state.backID?.data?.length || 0,
|
|
434
|
+
compressionRatio: compressedFrontImage.length / (this.state.frontID.data?.length || 1)
|
|
400
435
|
});
|
|
401
436
|
|
|
402
437
|
const response = await this.backendClient.fullValidation({
|
|
403
|
-
frontIdImage:
|
|
404
|
-
backIdImage:
|
|
405
|
-
videoFrames:
|
|
438
|
+
frontIdImage: compressedFrontImage,
|
|
439
|
+
backIdImage: compressedBackImage,
|
|
440
|
+
videoFrames: validFrames,
|
|
406
441
|
videoDurationMs: this.state.videoData.duration,
|
|
407
442
|
challengesCompleted: this.state.videoData.challengesCompleted || [],
|
|
408
443
|
});
|
|
@@ -486,6 +521,107 @@ export class BiometricIdentitySDK {
|
|
|
486
521
|
});
|
|
487
522
|
}
|
|
488
523
|
|
|
524
|
+
/**
|
|
525
|
+
* Compress base64 image - simplified version that works in all environments
|
|
526
|
+
* For React Native, images should be compressed at capture time
|
|
527
|
+
* This is a no-op for now - compression should happen at capture time in React Native
|
|
528
|
+
*/
|
|
529
|
+
private async compressImage(base64Image: string): Promise<string> {
|
|
530
|
+
if (!base64Image || base64Image.length === 0) {
|
|
531
|
+
return base64Image;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const MAX_SIZE = 1024 * 1024;
|
|
535
|
+
if (base64Image.length < MAX_SIZE) {
|
|
536
|
+
return base64Image;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
const globalWindow = (globalThis as any).window;
|
|
541
|
+
if (globalWindow && globalWindow.Image && globalWindow.document) {
|
|
542
|
+
return await this.compressImageBrowser(base64Image, globalWindow);
|
|
543
|
+
}
|
|
544
|
+
} catch (error) {
|
|
545
|
+
logger.warn('Image compression not available, using original', error);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return base64Image;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Compress image in browser environment using Canvas API
|
|
553
|
+
*/
|
|
554
|
+
private async compressImageBrowser(base64Image: string, window: any): Promise<string> {
|
|
555
|
+
return new Promise((resolve, reject) => {
|
|
556
|
+
const img = new window.Image();
|
|
557
|
+
img.onload = () => {
|
|
558
|
+
const canvas = window.document.createElement('canvas');
|
|
559
|
+
const maxWidth = 1920;
|
|
560
|
+
const maxHeight = 1920;
|
|
561
|
+
let width = img.width;
|
|
562
|
+
let height = img.height;
|
|
563
|
+
|
|
564
|
+
if (width > maxWidth || height > maxHeight) {
|
|
565
|
+
const ratio = Math.min(maxWidth / width, maxHeight / height);
|
|
566
|
+
width = width * ratio;
|
|
567
|
+
height = height * ratio;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
canvas.width = width;
|
|
571
|
+
canvas.height = height;
|
|
572
|
+
|
|
573
|
+
const ctx = canvas.getContext('2d');
|
|
574
|
+
if (!ctx) {
|
|
575
|
+
reject(new Error('Could not get canvas context'));
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
580
|
+
|
|
581
|
+
const compressedBase64 = canvas.toDataURL('image/jpeg', 0.85);
|
|
582
|
+
const base64 = compressedBase64.split(',')[1];
|
|
583
|
+
resolve(base64);
|
|
584
|
+
};
|
|
585
|
+
img.onerror = reject;
|
|
586
|
+
img.src = `data:image/jpeg;base64,${base64Image}`;
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Fix base64 padding issues
|
|
592
|
+
*/
|
|
593
|
+
private fixBase64Padding(base64: string): string {
|
|
594
|
+
if (!base64) return base64;
|
|
595
|
+
|
|
596
|
+
let cleaned = base64.replace(/\s/g, '');
|
|
597
|
+
|
|
598
|
+
if (cleaned.includes(',')) {
|
|
599
|
+
cleaned = cleaned.split(',')[1] || cleaned;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const padding = cleaned.length % 4;
|
|
603
|
+
if (padding > 0) {
|
|
604
|
+
cleaned += '='.repeat(4 - padding);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return cleaned;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Validate if string is valid base64
|
|
612
|
+
*/
|
|
613
|
+
private isValidBase64(str: string): boolean {
|
|
614
|
+
if (!str || typeof str !== 'string') return false;
|
|
615
|
+
|
|
616
|
+
const cleaned = str.replace(/\s/g, '');
|
|
617
|
+
if (cleaned.includes(',')) {
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
622
|
+
return base64Regex.test(cleaned) && cleaned.length > 100;
|
|
623
|
+
}
|
|
624
|
+
|
|
489
625
|
/**
|
|
490
626
|
* Get current SDK state
|
|
491
627
|
*/
|