@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 = 30;
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: videoFrames.length,
317
+ framesCount: compressedFrames.length,
313
318
  duration: this.state.videoData.duration,
314
319
  challengesCount: this.state.videoData.challengesCompleted?.length || 0,
315
- frontImageSize: this.state.frontID.data?.length || 0,
316
- backImageSize: this.state.backID?.data?.length || 0
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: this.state.frontID.data,
320
- backIdImage: this.state.backID?.data,
321
- videoFrames: 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
- errorStack: error?.stack
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
- const text = await response.text();
208
- if (text) {
209
- errorData = JSON.parse(text);
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
- errorData = {};
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
- `Request failed with status ${response.status}`;
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
- errorData
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 new Error(errorMessage);
253
+ throw error;
230
254
  }
231
255
  return await response.json();
232
256
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hexar/biometric-identity-sdk-core",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "Core AI engine for biometric identity verification",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -381,7 +381,7 @@ export class BiometricIdentitySDK {
381
381
  });
382
382
  }
383
383
 
384
- const MAX_FRAMES = 30;
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: videoFrames.length,
403
+ framesCount: compressedFrames.length,
396
404
  duration: this.state.videoData.duration,
397
405
  challengesCount: this.state.videoData.challengesCompleted?.length || 0,
398
- frontImageSize: this.state.frontID.data?.length || 0,
399
- backImageSize: this.state.backID?.data?.length || 0
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: this.state.frontID.data,
404
- backIdImage: this.state.backID?.data,
405
- videoFrames: 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
- errorStack: error?.stack
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
- error?.message || 'Backend validation failed',
422
- error
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
  */
@@ -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
- const text = await response.text();
403
- if (text) {
404
- errorData = JSON.parse(text);
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
- errorData = {};
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
- `Request failed with status ${response.status}`;
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
- errorData
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 new Error(errorMessage);
456
+ throw error;
430
457
  }
431
458
 
432
459
  return await response.json() as T;