@hexar/biometric-identity-sdk-core 1.0.13 → 1.0.15
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,16 @@ 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;
|
|
82
92
|
/**
|
|
83
93
|
* Get current SDK state
|
|
84
94
|
*/
|
|
@@ -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,25 @@ class BiometricIdentitySDK {
|
|
|
308
308
|
sampledCount: videoFrames.length
|
|
309
309
|
});
|
|
310
310
|
}
|
|
311
|
+
const compressedFrontImage = await this.compressImage(this.state.frontID.data);
|
|
312
|
+
const compressedBackImage = this.state.backID?.data
|
|
313
|
+
? await this.compressImage(this.state.backID.data)
|
|
314
|
+
: undefined;
|
|
315
|
+
const compressedFrames = await Promise.all(videoFrames.map(frame => this.compressImage(frame)));
|
|
311
316
|
logger_1.logger.info('Sending validation request to backend', {
|
|
312
|
-
framesCount:
|
|
317
|
+
framesCount: compressedFrames.length,
|
|
313
318
|
duration: this.state.videoData.duration,
|
|
314
319
|
challengesCount: this.state.videoData.challengesCompleted?.length || 0,
|
|
315
|
-
frontImageSize:
|
|
316
|
-
backImageSize:
|
|
320
|
+
frontImageSize: compressedFrontImage.length,
|
|
321
|
+
backImageSize: compressedBackImage?.length || 0,
|
|
322
|
+
originalFrontSize: this.state.frontID.data?.length || 0,
|
|
323
|
+
originalBackSize: this.state.backID?.data?.length || 0,
|
|
324
|
+
compressionRatio: compressedFrontImage.length / (this.state.frontID.data?.length || 1)
|
|
317
325
|
});
|
|
318
326
|
const response = await this.backendClient.fullValidation({
|
|
319
|
-
frontIdImage:
|
|
320
|
-
backIdImage:
|
|
321
|
-
videoFrames:
|
|
327
|
+
frontIdImage: compressedFrontImage,
|
|
328
|
+
backIdImage: compressedBackImage,
|
|
329
|
+
videoFrames: compressedFrames,
|
|
322
330
|
videoDurationMs: this.state.videoData.duration,
|
|
323
331
|
challengesCompleted: this.state.videoData.challengesCompleted || [],
|
|
324
332
|
});
|
|
@@ -329,9 +337,19 @@ class BiometricIdentitySDK {
|
|
|
329
337
|
logger_1.logger.error('Backend validation failed', {
|
|
330
338
|
errorMessage: error?.message,
|
|
331
339
|
errorName: error?.name,
|
|
332
|
-
|
|
340
|
+
status: error?.status,
|
|
341
|
+
statusText: error?.statusText,
|
|
342
|
+
errorData: error?.errorData,
|
|
343
|
+
errorStack: error?.stack?.substring(0, 500)
|
|
344
|
+
});
|
|
345
|
+
const errorMessage = error?.message ||
|
|
346
|
+
(error?.status ? `Backend request failed with status ${error.status}` : 'Backend validation failed');
|
|
347
|
+
throw this.createError(types_1.BiometricErrorCode.NETWORK_ERROR, errorMessage, {
|
|
348
|
+
status: error?.status,
|
|
349
|
+
statusText: error?.statusText,
|
|
350
|
+
errorData: error?.errorData,
|
|
351
|
+
originalError: error?.message
|
|
333
352
|
});
|
|
334
|
-
throw this.createError(types_1.BiometricErrorCode.NETWORK_ERROR, error?.message || 'Backend validation failed', error);
|
|
335
353
|
}
|
|
336
354
|
}
|
|
337
355
|
/**
|
|
@@ -377,6 +395,63 @@ class BiometricIdentitySDK {
|
|
|
377
395
|
reader.readAsDataURL(blob);
|
|
378
396
|
});
|
|
379
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* Compress base64 image - simplified version that works in all environments
|
|
400
|
+
* For React Native, images should be compressed at capture time
|
|
401
|
+
* This is a no-op for now - compression should happen at capture time in React Native
|
|
402
|
+
*/
|
|
403
|
+
async compressImage(base64Image) {
|
|
404
|
+
if (!base64Image || base64Image.length === 0) {
|
|
405
|
+
return base64Image;
|
|
406
|
+
}
|
|
407
|
+
const MAX_SIZE = 1024 * 1024;
|
|
408
|
+
if (base64Image.length < MAX_SIZE) {
|
|
409
|
+
return base64Image;
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
const globalWindow = globalThis.window;
|
|
413
|
+
if (globalWindow && globalWindow.Image && globalWindow.document) {
|
|
414
|
+
return await this.compressImageBrowser(base64Image, globalWindow);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
logger_1.logger.warn('Image compression not available, using original', error);
|
|
419
|
+
}
|
|
420
|
+
return base64Image;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Compress image in browser environment using Canvas API
|
|
424
|
+
*/
|
|
425
|
+
async compressImageBrowser(base64Image, window) {
|
|
426
|
+
return new Promise((resolve, reject) => {
|
|
427
|
+
const img = new window.Image();
|
|
428
|
+
img.onload = () => {
|
|
429
|
+
const canvas = window.document.createElement('canvas');
|
|
430
|
+
const maxWidth = 1920;
|
|
431
|
+
const maxHeight = 1920;
|
|
432
|
+
let width = img.width;
|
|
433
|
+
let height = img.height;
|
|
434
|
+
if (width > maxWidth || height > maxHeight) {
|
|
435
|
+
const ratio = Math.min(maxWidth / width, maxHeight / height);
|
|
436
|
+
width = width * ratio;
|
|
437
|
+
height = height * ratio;
|
|
438
|
+
}
|
|
439
|
+
canvas.width = width;
|
|
440
|
+
canvas.height = height;
|
|
441
|
+
const ctx = canvas.getContext('2d');
|
|
442
|
+
if (!ctx) {
|
|
443
|
+
reject(new Error('Could not get canvas context'));
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
447
|
+
const compressedBase64 = canvas.toDataURL('image/jpeg', 0.85);
|
|
448
|
+
const base64 = compressedBase64.split(',')[1];
|
|
449
|
+
resolve(base64);
|
|
450
|
+
};
|
|
451
|
+
img.onerror = reject;
|
|
452
|
+
img.src = `data:image/jpeg;base64,${base64Image}`;
|
|
453
|
+
});
|
|
454
|
+
}
|
|
380
455
|
/**
|
|
381
456
|
* Get current SDK state
|
|
382
457
|
*/
|
|
@@ -184,6 +184,12 @@ class BackendClient {
|
|
|
184
184
|
*/
|
|
185
185
|
async request(endpoint, method, body) {
|
|
186
186
|
const url = `${this.config.apiEndpoint}${endpoint}`;
|
|
187
|
+
logger_1.logger.info('Making backend request', {
|
|
188
|
+
url,
|
|
189
|
+
method,
|
|
190
|
+
hasBody: !!body,
|
|
191
|
+
bodySize: body ? JSON.stringify(body).length : 0
|
|
192
|
+
});
|
|
187
193
|
const controller = new AbortController();
|
|
188
194
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
189
195
|
const headers = {
|
|
@@ -200,33 +206,51 @@ class BackendClient {
|
|
|
200
206
|
body: body ? JSON.stringify(body) : undefined,
|
|
201
207
|
signal: controller.signal,
|
|
202
208
|
});
|
|
209
|
+
logger_1.logger.info('Backend response received', {
|
|
210
|
+
url,
|
|
211
|
+
status: response.status,
|
|
212
|
+
statusText: response.statusText,
|
|
213
|
+
ok: response.ok
|
|
214
|
+
});
|
|
203
215
|
clearTimeout(timeoutId);
|
|
204
216
|
if (!response.ok) {
|
|
217
|
+
let errorText = '';
|
|
205
218
|
let errorData = {};
|
|
206
219
|
try {
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
|
|
220
|
+
errorText = await response.text();
|
|
221
|
+
if (errorText) {
|
|
222
|
+
try {
|
|
223
|
+
errorData = JSON.parse(errorText);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
errorData = { raw: errorText };
|
|
227
|
+
}
|
|
210
228
|
}
|
|
211
229
|
}
|
|
212
|
-
catch {
|
|
213
|
-
|
|
230
|
+
catch (parseError) {
|
|
231
|
+
logger_1.logger.warn('Could not parse error response', parseError);
|
|
214
232
|
}
|
|
215
233
|
const errorMessage = errorData?.error?.message ||
|
|
216
234
|
errorData?.detail ||
|
|
217
235
|
errorData?.message ||
|
|
218
|
-
|
|
236
|
+
errorData?.raw ||
|
|
237
|
+
`Request failed with status ${response.status}: ${response.statusText}`;
|
|
219
238
|
logger_1.logger.error('Backend request failed', {
|
|
220
239
|
url,
|
|
221
240
|
method,
|
|
222
241
|
status: response.status,
|
|
223
242
|
statusText: response.statusText,
|
|
224
|
-
|
|
243
|
+
errorText: errorText.substring(0, 500),
|
|
244
|
+
errorData: JSON.stringify(errorData).substring(0, 500)
|
|
225
245
|
});
|
|
246
|
+
const error = new Error(errorMessage);
|
|
247
|
+
error.status = response.status;
|
|
248
|
+
error.statusText = response.statusText;
|
|
249
|
+
error.errorData = errorData;
|
|
226
250
|
if (response.status === 413) {
|
|
227
251
|
throw new Error(`Payload too large (413). The request body exceeds the server's size limit. Try reducing the number of video frames or image sizes.`);
|
|
228
252
|
}
|
|
229
|
-
throw
|
|
253
|
+
throw error;
|
|
230
254
|
}
|
|
231
255
|
return await response.json();
|
|
232
256
|
}
|
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,29 @@ export class BiometricIdentitySDK {
|
|
|
391
391
|
});
|
|
392
392
|
}
|
|
393
393
|
|
|
394
|
+
const compressedFrontImage = await this.compressImage(this.state.frontID.data);
|
|
395
|
+
const compressedBackImage = this.state.backID?.data
|
|
396
|
+
? await this.compressImage(this.state.backID.data)
|
|
397
|
+
: undefined;
|
|
398
|
+
const compressedFrames = await Promise.all(
|
|
399
|
+
videoFrames.map(frame => this.compressImage(frame))
|
|
400
|
+
);
|
|
401
|
+
|
|
394
402
|
logger.info('Sending validation request to backend', {
|
|
395
|
-
framesCount:
|
|
403
|
+
framesCount: compressedFrames.length,
|
|
396
404
|
duration: this.state.videoData.duration,
|
|
397
405
|
challengesCount: this.state.videoData.challengesCompleted?.length || 0,
|
|
398
|
-
frontImageSize:
|
|
399
|
-
backImageSize:
|
|
406
|
+
frontImageSize: compressedFrontImage.length,
|
|
407
|
+
backImageSize: compressedBackImage?.length || 0,
|
|
408
|
+
originalFrontSize: this.state.frontID.data?.length || 0,
|
|
409
|
+
originalBackSize: this.state.backID?.data?.length || 0,
|
|
410
|
+
compressionRatio: compressedFrontImage.length / (this.state.frontID.data?.length || 1)
|
|
400
411
|
});
|
|
401
412
|
|
|
402
413
|
const response = await this.backendClient.fullValidation({
|
|
403
|
-
frontIdImage:
|
|
404
|
-
backIdImage:
|
|
405
|
-
videoFrames:
|
|
414
|
+
frontIdImage: compressedFrontImage,
|
|
415
|
+
backIdImage: compressedBackImage,
|
|
416
|
+
videoFrames: compressedFrames,
|
|
406
417
|
videoDurationMs: this.state.videoData.duration,
|
|
407
418
|
challengesCompleted: this.state.videoData.challengesCompleted || [],
|
|
408
419
|
});
|
|
@@ -414,12 +425,24 @@ export class BiometricIdentitySDK {
|
|
|
414
425
|
logger.error('Backend validation failed', {
|
|
415
426
|
errorMessage: error?.message,
|
|
416
427
|
errorName: error?.name,
|
|
417
|
-
|
|
428
|
+
status: error?.status,
|
|
429
|
+
statusText: error?.statusText,
|
|
430
|
+
errorData: error?.errorData,
|
|
431
|
+
errorStack: error?.stack?.substring(0, 500)
|
|
418
432
|
});
|
|
433
|
+
|
|
434
|
+
const errorMessage = error?.message ||
|
|
435
|
+
(error?.status ? `Backend request failed with status ${error.status}` : 'Backend validation failed');
|
|
436
|
+
|
|
419
437
|
throw this.createError(
|
|
420
438
|
BiometricErrorCode.NETWORK_ERROR,
|
|
421
|
-
|
|
422
|
-
|
|
439
|
+
errorMessage,
|
|
440
|
+
{
|
|
441
|
+
status: error?.status,
|
|
442
|
+
statusText: error?.statusText,
|
|
443
|
+
errorData: error?.errorData,
|
|
444
|
+
originalError: error?.message
|
|
445
|
+
}
|
|
423
446
|
);
|
|
424
447
|
}
|
|
425
448
|
}
|
|
@@ -474,6 +497,72 @@ export class BiometricIdentitySDK {
|
|
|
474
497
|
});
|
|
475
498
|
}
|
|
476
499
|
|
|
500
|
+
/**
|
|
501
|
+
* Compress base64 image - simplified version that works in all environments
|
|
502
|
+
* For React Native, images should be compressed at capture time
|
|
503
|
+
* This is a no-op for now - compression should happen at capture time in React Native
|
|
504
|
+
*/
|
|
505
|
+
private async compressImage(base64Image: string): Promise<string> {
|
|
506
|
+
if (!base64Image || base64Image.length === 0) {
|
|
507
|
+
return base64Image;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const MAX_SIZE = 1024 * 1024;
|
|
511
|
+
if (base64Image.length < MAX_SIZE) {
|
|
512
|
+
return base64Image;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
try {
|
|
516
|
+
const globalWindow = (globalThis as any).window;
|
|
517
|
+
if (globalWindow && globalWindow.Image && globalWindow.document) {
|
|
518
|
+
return await this.compressImageBrowser(base64Image, globalWindow);
|
|
519
|
+
}
|
|
520
|
+
} catch (error) {
|
|
521
|
+
logger.warn('Image compression not available, using original', error);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return base64Image;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Compress image in browser environment using Canvas API
|
|
529
|
+
*/
|
|
530
|
+
private async compressImageBrowser(base64Image: string, window: any): Promise<string> {
|
|
531
|
+
return new Promise((resolve, reject) => {
|
|
532
|
+
const img = new window.Image();
|
|
533
|
+
img.onload = () => {
|
|
534
|
+
const canvas = window.document.createElement('canvas');
|
|
535
|
+
const maxWidth = 1920;
|
|
536
|
+
const maxHeight = 1920;
|
|
537
|
+
let width = img.width;
|
|
538
|
+
let height = img.height;
|
|
539
|
+
|
|
540
|
+
if (width > maxWidth || height > maxHeight) {
|
|
541
|
+
const ratio = Math.min(maxWidth / width, maxHeight / height);
|
|
542
|
+
width = width * ratio;
|
|
543
|
+
height = height * ratio;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
canvas.width = width;
|
|
547
|
+
canvas.height = height;
|
|
548
|
+
|
|
549
|
+
const ctx = canvas.getContext('2d');
|
|
550
|
+
if (!ctx) {
|
|
551
|
+
reject(new Error('Could not get canvas context'));
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
556
|
+
|
|
557
|
+
const compressedBase64 = canvas.toDataURL('image/jpeg', 0.85);
|
|
558
|
+
const base64 = compressedBase64.split(',')[1];
|
|
559
|
+
resolve(base64);
|
|
560
|
+
};
|
|
561
|
+
img.onerror = reject;
|
|
562
|
+
img.src = `data:image/jpeg;base64,${base64Image}`;
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
477
566
|
/**
|
|
478
567
|
* Get current SDK state
|
|
479
568
|
*/
|
package/src/api/BackendClient.ts
CHANGED
|
@@ -374,6 +374,13 @@ export class BackendClient {
|
|
|
374
374
|
): Promise<T> {
|
|
375
375
|
const url = `${this.config.apiEndpoint}${endpoint}`;
|
|
376
376
|
|
|
377
|
+
logger.info('Making backend request', {
|
|
378
|
+
url,
|
|
379
|
+
method,
|
|
380
|
+
hasBody: !!body,
|
|
381
|
+
bodySize: body ? JSON.stringify(body).length : 0
|
|
382
|
+
});
|
|
383
|
+
|
|
377
384
|
const controller = new AbortController();
|
|
378
385
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
379
386
|
|
|
@@ -393,40 +400,60 @@ export class BackendClient {
|
|
|
393
400
|
body: body ? JSON.stringify(body) : undefined,
|
|
394
401
|
signal: controller.signal as RequestInit['signal'],
|
|
395
402
|
});
|
|
403
|
+
|
|
404
|
+
logger.info('Backend response received', {
|
|
405
|
+
url,
|
|
406
|
+
status: response.status,
|
|
407
|
+
statusText: response.statusText,
|
|
408
|
+
ok: response.ok
|
|
409
|
+
});
|
|
396
410
|
|
|
397
411
|
clearTimeout(timeoutId);
|
|
398
412
|
|
|
399
413
|
if (!response.ok) {
|
|
414
|
+
let errorText = '';
|
|
400
415
|
let errorData: Record<string, any> = {};
|
|
416
|
+
|
|
401
417
|
try {
|
|
402
|
-
|
|
403
|
-
if (
|
|
404
|
-
|
|
418
|
+
errorText = await response.text();
|
|
419
|
+
if (errorText) {
|
|
420
|
+
try {
|
|
421
|
+
errorData = JSON.parse(errorText);
|
|
422
|
+
} catch {
|
|
423
|
+
errorData = { raw: errorText };
|
|
424
|
+
}
|
|
405
425
|
}
|
|
406
|
-
} catch {
|
|
407
|
-
|
|
426
|
+
} catch (parseError) {
|
|
427
|
+
logger.warn('Could not parse error response', parseError);
|
|
408
428
|
}
|
|
409
429
|
|
|
410
430
|
const errorMessage = errorData?.error?.message ||
|
|
411
431
|
errorData?.detail ||
|
|
412
432
|
errorData?.message ||
|
|
413
|
-
|
|
433
|
+
errorData?.raw ||
|
|
434
|
+
`Request failed with status ${response.status}: ${response.statusText}`;
|
|
414
435
|
|
|
415
436
|
logger.error('Backend request failed', {
|
|
416
437
|
url,
|
|
417
438
|
method,
|
|
418
439
|
status: response.status,
|
|
419
440
|
statusText: response.statusText,
|
|
420
|
-
|
|
441
|
+
errorText: errorText.substring(0, 500),
|
|
442
|
+
errorData: JSON.stringify(errorData).substring(0, 500)
|
|
421
443
|
});
|
|
422
444
|
|
|
445
|
+
const error = new Error(errorMessage);
|
|
446
|
+
(error as any).status = response.status;
|
|
447
|
+
(error as any).statusText = response.statusText;
|
|
448
|
+
(error as any).errorData = errorData;
|
|
449
|
+
|
|
423
450
|
if (response.status === 413) {
|
|
424
451
|
throw new Error(
|
|
425
452
|
`Payload too large (413). The request body exceeds the server's size limit. Try reducing the number of video frames or image sizes.`
|
|
426
453
|
);
|
|
427
454
|
}
|
|
428
455
|
|
|
429
|
-
throw
|
|
456
|
+
throw error;
|
|
430
457
|
}
|
|
431
458
|
|
|
432
459
|
return await response.json() as T;
|