@sanctum-key/react-native-sdk 1.0.32 → 1.0.34

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.
@@ -23,19 +23,17 @@ const documentTypeMap: Record<string, string> = {
23
23
  healthInsuranceCard: "health_insurance_card",
24
24
  residencePermit: "permanent_residence_card",
25
25
  identityCard: "identity_card",
26
-
27
- nationalId: "identity_card", // We saw this in your earlier logs!
28
- national_id: "identity_card",
29
-
30
- drivers_licence: "driving_licence", // Plural 'drivers' to singular 'driving'
31
- drivers_license: "driving_licence", // American spelling 'license'
32
- driving_license: "driving_licence", // American spelling 'license'
33
- permanent_residence: "permanent_residence_card", // Missing the word 'card'
26
+ nationalId: "identity_card",
27
+ national_id: "identity_card",
28
+ drivers_licence: "driving_licence",
29
+ drivers_license: "driving_licence",
30
+ driving_license: "driving_licence",
31
+ permanent_residence: "permanent_residence_card",
34
32
  };
35
33
 
36
34
  let cachedToken: string | null = null;
37
35
  let tokenExpiryTime: number | null = null;
38
- let activeAuthRequest: Promise<string> | null = null;
36
+ let activeAuthRequest: Promise<string> | null = null;
39
37
 
40
38
  export interface KYCRequest {
41
39
  userId: string;
@@ -71,11 +69,10 @@ export interface SelfieVideoResponse {
71
69
  error: string;
72
70
  }
73
71
 
74
-
75
72
  export class KYCService {
76
73
  private baseURL: string;
77
74
  private apiKey: string;
78
- // Additional service base URLs (fixed as per current infra)
75
+
79
76
  private faceServiceURL = 'https://face-infera.sanctumkey.com';
80
77
  private textExtractionServiceURL = 'https://text-infera.sanctumkey.com';
81
78
  private mrzServiceURL = 'https://mrz-infera.sanctumkey.com';
@@ -150,7 +147,6 @@ export class KYCService {
150
147
  }
151
148
 
152
149
  async processSelfieOrientationPicture(videoFile: string, token: string): Promise<SelfieVideoResponse[]> {
153
-
154
150
  try {
155
151
  const formData = new FormData();
156
152
  await appendFileToFormData(formData, 'file', videoFile, 'selfie_picture.jpg', 'image/jpeg');
@@ -160,7 +156,10 @@ export class KYCService {
160
156
  formData,
161
157
  {
162
158
  timeout: 20000,
163
- headers: { 'Content-Type': 'multipart/form-data', 'Authorization': `Bearer ${token}`, },
159
+ headers: {
160
+ // 🚨 Removed explicit Content-Type to allow boundary injection
161
+ 'Authorization': `Bearer ${token}`,
162
+ },
164
163
  }
165
164
  );
166
165
  return response.data;
@@ -172,7 +171,6 @@ export class KYCService {
172
171
 
173
172
  async processOrientationVideo(videoFile: string, env: KycEnvironment = 'PRODUCTION'): Promise<OrientationVideoResponse> {
174
173
  try {
175
- // SANDBOX mode: skip AI verification and return mock response
176
174
  if (env === 'SANDBOX') {
177
175
  console.log("SANDBOX mode: Skipping AI orientation video processing");
178
176
  logger.log("SANDBOX mode: Returning mock orientation video response");
@@ -187,11 +185,9 @@ export class KYCService {
187
185
  };
188
186
  }
189
187
 
190
- // Create FormData for multipart/form-data request
191
188
  const formData = new FormData();
192
189
  await appendFileToFormData(formData, 'file', videoFile, 'orientation_video.mp4', 'video/mp4');
193
190
 
194
-
195
191
  const response = await axios.post(
196
192
  `${this.orientationServiceURL}/process_orientation_video_stream/`,
197
193
  formData,
@@ -208,8 +204,6 @@ export class KYCService {
208
204
  };
209
205
  } catch (error: any) {
210
206
  logger.error('Error processing orientation video:', JSON.stringify(error, null, 2));
211
-
212
- // Handle specific error cases
213
207
  if (error.response?.status === 503) {
214
208
  return {
215
209
  success: false,
@@ -221,7 +215,6 @@ export class KYCService {
221
215
  message: 'Service temporarily unavailable'
222
216
  };
223
217
  }
224
-
225
218
  return {
226
219
  success: false,
227
220
  error: {
@@ -236,13 +229,11 @@ export class KYCService {
236
229
 
237
230
  // STEP 1 - ID CARD VALIDATION
238
231
  async detectFaceOnId(
239
- idCardImageUri: string,
240
- token: string,
241
- docType: string,
232
+ idCardImageUri: string,
233
+ token: string,
234
+ docType: string,
242
235
  env: KycEnvironment = 'PRODUCTION'
243
- ): Promise<{ result: boolean; detail: any[]; card_obb?: any }> { // Added card_obb to signature
244
-
245
- // SANDBOX mode
236
+ ): Promise<{ result: boolean; detail: any[]; card_obb?: any }> {
246
237
  if (env === 'SANDBOX') {
247
238
  logger.log("SANDBOX mode: Returning mock face detection response");
248
239
  return {
@@ -251,39 +242,33 @@ export class KYCService {
251
242
  card_obb: { x: 50, y: 50, width: 200, height: 200 }
252
243
  };
253
244
  }
254
-
245
+
255
246
  const formData = new FormData();
256
247
  await appendFileToFormData(formData, 'file', idCardImageUri, 'id_card_photo.jpg', 'image/jpeg');
257
-
258
- const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType] || docType;
259
248
 
260
- // Log metadata, NOT the FormData object
249
+ const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType] || docType;
261
250
  logger.log('detectFaceOnId Request:', { docTypeShorted, imageUri: idCardImageUri });
262
-
251
+
263
252
  try {
264
253
  const res = await axios.post(
265
- `${this.faceServiceURL}/detect_face/?doc_type=${docTypeShorted}`,
254
+ `${this.faceServiceURL}/detect_face/?doc_type=${docTypeShorted}`,
266
255
  formData,
267
256
  {
268
- headers: {
269
- 'Content-Type': 'multipart/form-data',
270
- 'Authorization': `Bearer ${token}`
257
+ headers: {
258
+ // 🚨 Removed explicit Content-Type to allow boundary injection
259
+ 'Authorization': `Bearer ${token}`
271
260
  },
272
- timeout: 20000 // 20 seconds is good, but ensure the image is compressed before this step!
261
+ timeout: 20000
273
262
  }
274
263
  );
275
264
 
276
- // It's safe to stringify res.data if the backend returns a clean JSON object
277
265
  logger.log('detectFaceOnId res', JSON.stringify(res.data, null, 2));
278
-
266
+
279
267
  if (res.data?.result) {
280
268
  return res.data;
281
269
  }
282
-
283
270
  throw new Error(res.data?.detail || 'detect_face failed');
284
-
285
271
  } catch (error: any) {
286
- // Use your safe error extractor, avoid JSON.stringify on raw Axios errors
287
272
  const safeError = errorMessage(error) || error.message;
288
273
  logger.error('Error detecting face on id:', safeError);
289
274
  throw error;
@@ -292,18 +277,16 @@ export class KYCService {
292
277
 
293
278
  //check templatetemplate_type
294
279
  async checkTemplateType(
295
- params: {
296
- fileUri: string;
297
- docType: string;
298
- docRegion: string;
299
- token: string;
280
+ params: {
281
+ fileUri: string;
282
+ docType: string;
283
+ docRegion: string;
284
+ token: string;
300
285
  postfix: string;
301
- issuingSubdivision?: string;
302
- },
286
+ issuingSubdivision?: string;
287
+ },
303
288
  env: KycEnvironment = 'PRODUCTION'
304
289
  ): Promise<any> {
305
-
306
- // SANDBOX mode
307
290
  if (env === 'SANDBOX') {
308
291
  console.log("SANDBOX mode: Skipping AI template type check");
309
292
  logger.log("SANDBOX mode: Returning mock template type response");
@@ -315,26 +298,28 @@ export class KYCService {
315
298
  }
316
299
 
317
300
  const { fileUri, docType, docRegion, token, postfix, issuingSubdivision } = params;
301
+
318
302
  const formData = new FormData();
319
-
320
303
  await appendFileToFormData(formData, 'file', fileUri, `id_card_${postfix}.jpg`, 'image/jpeg');
321
304
 
322
305
  const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType];
323
306
 
324
- // Build the base URL
325
307
  let url = `${this.mrzServiceURL}/get_template_version/?doc_type=${encodeURIComponent(docTypeShorted)}&doc_region=${encodeURIComponent(docRegion)}&postfix=${postfix}`;
326
-
327
308
  if (issuingSubdivision) {
328
309
  url += `&issuing_subdivision=${encodeURIComponent(issuingSubdivision)}`;
329
310
  }
330
311
 
331
312
  logger.log('checkTemplateType params', this.mrzServiceURL, JSON.stringify({ fileUri, docTypeShorted, docRegion, issuingSubdivision, token, postfix, url }, null, 2));
332
-
313
+
333
314
  try {
334
315
  const res = await axios.post<CheckTemplateTypeResponse>(url, formData, {
335
- headers: { 'Content-Type': 'multipart/form-data', 'Authorization': `Bearer ${token}`, },
316
+ headers: {
317
+ // 🚨 Removed explicit Content-Type to allow boundary injection
318
+ 'Authorization': `Bearer ${token}`,
319
+ },
336
320
  timeout: 60000,
337
321
  });
322
+
338
323
  logger.log('checkTemplateType res', JSON.stringify(res.data, null, 2));
339
324
  return res.data;
340
325
  } catch (e: any) {
@@ -343,69 +328,62 @@ export class KYCService {
343
328
  }
344
329
  }
345
330
 
346
- async extractDocumentInformation(params: {
347
- fileUri: string;
348
- docType: string;
349
- docRegion: string;
350
- templatePath: string;
331
+ async extractDocumentInformation(params: {
332
+ fileUri: string;
333
+ docType: string;
334
+ docRegion: string;
335
+ templatePath: string;
351
336
  issuingSubdivision?: string;
352
- token: string;
353
- postfix?: string
337
+ token: string;
338
+ postfix?: string
354
339
  }): Promise<any> {
355
340
  const { fileUri, docType, docRegion, templatePath, issuingSubdivision, token, postfix = 'front' } = params;
356
-
357
- // 1. Prepare Form Data
341
+
358
342
  const formData = new FormData();
359
343
  await appendFileToFormData(formData, 'file', fileUri, `id_card_${postfix}.jpg`, 'image/jpeg');
360
344
 
361
345
  let queryParams = `doc_type=${encodeURIComponent(docType)}&doc_region=${encodeURIComponent(docRegion)}&template_path=${encodeURIComponent(templatePath)}&postfix=${postfix}&cleaned=true`;
362
-
363
346
  if (issuingSubdivision) {
364
347
  queryParams += `&issuing_subdivision=${encodeURIComponent(issuingSubdivision)}`;
365
348
  }
349
+
366
350
  logger.log(`URL PARAMS: ${queryParams}`)
367
-
368
351
  const url = `${this.textExtractionServiceURL}/extract_doc_information/?${queryParams}`;
369
352
 
370
353
  try {
371
354
  const startRes = await axios.post(url, formData, {
372
- headers: { 'Content-Type': 'multipart/form-data', 'Authorization': `Bearer ${token}` },
355
+ headers: {
356
+ // 🚨 Removed explicit Content-Type to allow boundary injection
357
+ 'Authorization': `Bearer ${token}`
358
+ },
373
359
  timeout: 60000,
374
360
  });
375
-
361
+
376
362
  const jobId = startRes.data?.job_id;
377
363
  if (!jobId) throw new Error("Backend did not return a job_id");
378
-
379
364
  logger.log(`Extraction job started: ${jobId}. Polling for results...`);
380
365
 
381
- // 4. Poll the Status Endpoint
382
366
  const statusUrl = `${this.textExtractionServiceURL}/extract_doc_information/status/${jobId}`;
383
-
384
- // Poll every 2 seconds, up to 15 times (30 seconds max)
385
367
  const maxRetries = 15;
368
+
386
369
  for (let i = 0; i < maxRetries; i++) {
387
- // Wait 2 seconds before checking
388
370
  await new Promise(resolve => setTimeout(resolve, 2000));
389
-
390
371
  const statusRes = await axios.get(statusUrl, {
391
- headers: { 'Authorization': `Bearer ${token}` }
372
+ headers: {
373
+ 'Authorization': `Bearer ${token}`
374
+ }
392
375
  });
393
-
394
376
  const statusData = statusRes.data;
395
377
 
396
378
  if (statusData.status === 'completed') {
397
379
  logger.log('Extraction completed successfully', JSON.stringify(truncateFields(statusData.result)));
398
- return statusData.result;
380
+ return statusData.result;
399
381
  }
400
-
401
382
  if (statusData.status === 'failed') {
402
383
  throw new Error(statusData.error || "Document extraction failed during processing.");
403
384
  }
404
-
405
385
  }
406
-
407
386
  throw new Error("Document extraction timed out waiting for results.");
408
-
409
387
  } catch (e: any) {
410
388
  const errorMsg = e.response?.data?.detail || e.message;
411
389
  logger.error('Error extracting document information:', errorMsg);
@@ -414,20 +392,16 @@ export class KYCService {
414
392
  }
415
393
 
416
394
  async extractBarcode(params: { fileUri: string; token: string; docType: string; postfix?: string }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
417
- // SANDBOX mode
418
395
  if (env === 'SANDBOX') {
419
396
  console.log("SANDBOX mode: Skipping AI barcode extraction");
420
397
  logger.log("SANDBOX mode: Returning mock barcode response");
421
- return {
422
- barcode_data: 'SANDBOX_MOCK_BARCODE_DATA',
423
- card_obb: { x: 50, y: 50, width: 200, height: 200 }
424
- };
398
+ return { barcode_data: 'SANDBOX_MOCK_BARCODE_DATA', card_obb: { x: 50, y: 50, width: 200, height: 200 } };
425
399
  }
426
400
 
427
401
  const { fileUri, token, docType, postfix = 'back' } = params;
428
402
  logger.log(docType)
429
- const formData = new FormData();
430
403
 
404
+ const formData = new FormData();
431
405
  await appendFileToFormData(formData, 'file', fileUri, `id_${postfix}_photo.jpg`, 'image/jpeg');
432
406
 
433
407
  const url = `${this.barcodeServiceURL}/decode_barcode/?doc_type=${encodeURIComponent(docType)}`;
@@ -435,13 +409,13 @@ export class KYCService {
435
409
  const attempt = async () => {
436
410
  try {
437
411
  const res = await axios.post(url, formData, {
438
- headers: { 'Content-Type': 'multipart/form-data', 'Authorization': `Bearer ${token}` },
412
+ headers: {
413
+ // 🚨 Removed explicit Content-Type to allow boundary injection
414
+ 'Authorization': `Bearer ${token}`
415
+ },
439
416
  timeout: 60000,
440
417
  });
441
-
442
418
  logger.log('📦 Raw extractBarcode response:', JSON.stringify(res.data, null, 2));
443
-
444
-
445
419
  return res.data;
446
420
  } catch (e: any) {
447
421
  throw new Error(e?.message || 'Erreur de détection du code-barres');
@@ -457,20 +431,15 @@ export class KYCService {
457
431
  }
458
432
 
459
433
  async formatBarcodeData(params: { textInput: string; token: string }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
460
- // SANDBOX mode
461
434
  if (env === 'SANDBOX') {
462
435
  console.log("SANDBOX mode: Skipping AI barcode formatting");
463
436
  logger.log("SANDBOX mode: Returning mock formatted barcode response");
464
- return {
465
- barcode_type: "SANDBOX_MOCK",
466
- first_name: "JOHN",
467
- last_name: "DOE",
468
- card_number: "000000000000"
469
- };
437
+ return { barcode_type: "SANDBOX_MOCK", first_name: "JOHN", last_name: "DOE", card_number: "000000000000" };
470
438
  }
471
439
 
472
440
  const { textInput, token } = params;
473
441
 
442
+ // URLSearchParams requires urlencoded, this remains untouched
474
443
  const formData = new URLSearchParams();
475
444
  formData.append('text_input', textInput);
476
445
 
@@ -479,15 +448,13 @@ export class KYCService {
479
448
  const attempt = async () => {
480
449
  try {
481
450
  const res = await axios.post(url, formData.toString(), {
482
- headers: {
483
- 'Content-Type': 'application/x-www-form-urlencoded',
484
- 'Authorization': `Bearer ${token}`
451
+ headers: {
452
+ 'Content-Type': 'application/x-www-form-urlencoded',
453
+ 'Authorization': `Bearer ${token}`
485
454
  },
486
455
  timeout: 60000,
487
456
  });
488
-
489
457
  logger.log('📦 Raw formatBarcodeData response:', JSON.stringify(res.data, null, 2));
490
-
491
458
  return res.data;
492
459
  } catch (e: any) {
493
460
  throw new Error(e?.message || 'Erreur de formatage du code-barres');
@@ -502,7 +469,6 @@ export class KYCService {
502
469
  }
503
470
  }
504
471
 
505
-
506
472
  async extractMrzText(
507
473
  params: {
508
474
  fileUri: string;
@@ -514,125 +480,107 @@ export class KYCService {
514
480
  mrz_type?: string;
515
481
  },
516
482
  env: KycEnvironment = 'PRODUCTION'
517
- ): Promise<any> {
518
- // SANDBOX mode
519
- if (env === 'SANDBOX') {
520
- console.log("SANDBOX mode: Skipping AI MRZ extraction");
521
- const { docType, docRegion, postfix = 'back', mrz_type } = params;
522
- return {
523
- success: true,
524
- parsed_data: {
525
- status: 'success',
526
- document_type: docType,
527
- mrz_type: mrz_type || 'TD1',
528
- doc_region: docRegion,
529
- postfix: postfix
530
- },
531
- card_obb: { x: 50, y: 50, width: 200, height: 200 }
532
- };
533
- }
534
- const { fileUri, docType, docRegion, postfix = 'back', token, template_path, mrz_type } = params;
535
-
536
- // ... top of extractMrzText (params extraction, etc) ...
537
- const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType] || docType;
538
- const safeMrzType = mrz_type && mrz_type.trim() !== '' ? mrz_type : 'TD1';
539
-
540
- logger.log("docTypeShorted", docTypeShorted, docRegion, postfix);
541
-
542
- const queryParams = new URLSearchParams({
543
- doc_type: docTypeShorted,
544
- doc_region: docRegion,
545
- postfix: postfix,
546
- template_path: template_path,
547
- mrz_type: safeMrzType,
548
- reset_cache: 'true'
549
- }).toString();
550
-
551
- const url = `${this.mrzServiceURL}/extract_mrz_text/?${queryParams}`;
552
- logger.log("url", url);
553
-
554
- try {
555
- let response;
556
-
557
- if (Platform.OS === 'web') {
558
- const nativeFetch = window.fetch;
559
- const NativeFormData = window.FormData;
560
-
561
- const formData = new NativeFormData();
562
-
563
- // Fetch the Blob natively
564
- const fileReq = await nativeFetch(fileUri);
565
- const blob = await fileReq.blob();
566
-
567
- // Append the Blob. The native browser will perfectly handle the multipart boundary.
568
- formData.append('file', blob, `id_card_${postfix}.jpg`);
569
-
570
- // Execute using Native Fetch!
571
- response = await nativeFetch(url, {
572
- method: 'POST',
573
- headers: {
574
- 'Authorization': `Bearer ${token}`,
575
- 'Accept': 'application/json'
576
- },
577
- body: formData,
578
- });
579
-
580
- } else {
581
- // 📱 MOBILE NATIVE: React Native's Native Bridge expects this exact object
582
- const formData = new FormData();
583
- const filePayload = {
584
- uri: fileUri,
585
- type: 'image/jpeg',
586
- name: `id_card_${postfix}.jpg`,
587
- };
588
- formData.append('file', filePayload as any);
589
-
590
- response = await fetch(url, {
591
- method: 'POST',
592
- headers: {
593
- 'Authorization': `Bearer ${token}`,
594
- 'Accept': 'application/json'
595
- },
596
- body: formData,
597
- });
598
- }
599
-
600
- if (!response.ok) {
601
- const errorText = await response.text();
602
- logger.error('Backend MRZ Error Details:', errorText);
603
- throw new Error(`Erreur serveur: ${response.status}`);
604
- }
605
-
606
- const data = await response.json();
607
- logger.log('extractMrzText res', JSON.stringify(data, null, 2));
608
-
609
- if (Object.keys(data).length === 0) throw new Error('No data found');
610
-
611
- if (data?.success === false) {
612
- const serverMessage = data.parsed_data?.status_message || data.parsed_data?.status || 'Échec de l\'extraction MRZ';
613
- throw new Error(`Lecture MRZ refusée: ${serverMessage}`);
614
- }
615
-
616
- return data;
617
-
618
- } catch (error: any) {
619
- logger.error("MRZ Fetch Error: ", error?.message || error);
620
- throw new Error(error?.message || 'Erreur de connexion lors de l\'extraction MRZ');
621
- }
622
- }
483
+ ): Promise<any> {
484
+ if (env === 'SANDBOX') {
485
+ console.log("SANDBOX mode: Skipping AI MRZ extraction");
486
+ const { docType, docRegion, postfix = 'back', mrz_type } = params;
487
+ return {
488
+ success: true,
489
+ parsed_data: {
490
+ status: 'success',
491
+ document_type: docType,
492
+ mrz_type: mrz_type || 'TD1',
493
+ doc_region: docRegion,
494
+ postfix: postfix
495
+ },
496
+ card_obb: { x: 50, y: 50, width: 200, height: 200 }
497
+ };
498
+ }
499
+
500
+ const { fileUri, docType, docRegion, postfix = 'back', token, template_path, mrz_type } = params;
501
+
502
+ const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType] || docType;
503
+ const safeMrzType = mrz_type && mrz_type.trim() !== '' ? mrz_type : 'TD1';
504
+
505
+ logger.log("docTypeShorted", docTypeShorted, docRegion, postfix);
506
+
507
+ const queryParams = new URLSearchParams({
508
+ doc_type: docTypeShorted,
509
+ doc_region: docRegion,
510
+ postfix: postfix,
511
+ template_path: template_path,
512
+ mrz_type: safeMrzType,
513
+ reset_cache: 'true'
514
+ }).toString();
515
+
516
+ const url = `${this.mrzServiceURL}/extract_mrz_text/?${queryParams}`;
517
+ logger.log("url", url);
518
+
519
+ try {
520
+ let response;
521
+
522
+ if (Platform.OS === 'web') {
523
+ const nativeFetch = window.fetch;
524
+ const NativeFormData = window.FormData;
525
+ const formData = new NativeFormData();
526
+ const fileReq = await nativeFetch(fileUri);
527
+ const blob = await fileReq.blob();
528
+ formData.append('file', blob, `id_card_${postfix}.jpg`);
529
+
530
+ response = await nativeFetch(url, {
531
+ method: 'POST',
532
+ headers: {
533
+ 'Authorization': `Bearer ${token}`,
534
+ 'Accept': 'application/json'
535
+ },
536
+ body: formData,
537
+ });
538
+ } else {
539
+ // 📱 NATIVE FETCH APPROACH (Untouched, robust as is)
540
+ const formData = new FormData();
541
+ const filePayload = {
542
+ uri: fileUri,
543
+ type: 'image/jpeg',
544
+ name: `id_card_${postfix}.jpg`,
545
+ };
546
+ formData.append('file', filePayload as any);
547
+
548
+ response = await fetch(url, {
549
+ method: 'POST',
550
+ headers: {
551
+ 'Authorization': `Bearer ${token}`,
552
+ 'Accept': 'application/json'
553
+ },
554
+ body: formData,
555
+ });
556
+ }
557
+
558
+ if (!response.ok) {
559
+ const errorText = await response.text();
560
+ logger.error('Backend MRZ Error Details:', errorText);
561
+ throw new Error(`Erreur serveur: ${response.status}`);
562
+ }
563
+ const data = await response.json();
564
+ logger.log('extractMrzText res', JSON.stringify(data, null, 2));
565
+
566
+ if (Object.keys(data).length === 0) throw new Error('No data found');
567
+ if (data?.success === false) {
568
+ const serverMessage = data.parsed_data?.status_message || data.parsed_data?.status || 'Échec de l\'extraction MRZ';
569
+ throw new Error(`Lecture MRZ refusée: ${serverMessage}`);
570
+ }
571
+ return data;
572
+ } catch (error: any) {
573
+ logger.error("MRZ Fetch Error: ", error?.message || error);
574
+ throw new Error(error?.message || 'Erreur de connexion lors de l\'extraction MRZ');
575
+ }
576
+ }
623
577
 
624
578
  // STEP 2 - SELFIE VALIDATION
625
579
  async recognizeFace(params: { idPhotoUri: string; selfiePhotoUri: string }, env: KycEnvironment = 'PRODUCTION'): Promise<{ is_match: boolean; similarity: number; id_bbox?: number[]; selfie_bbox?: number[] }> {
626
- // SANDBOX mode
627
580
  if (env === 'SANDBOX') {
628
581
  console.log("SANDBOX mode: Skipping AI face recognition");
629
582
  logger.log("SANDBOX mode: Returning mock face recognition response");
630
- return {
631
- is_match: true,
632
- similarity: 0.95,
633
- id_bbox: [50, 50, 200, 200],
634
- selfie_bbox: [50, 50, 200, 200]
635
- };
583
+ return { is_match: true, similarity: 0.95, id_bbox: [50, 50, 200, 200], selfie_bbox: [50, 50, 200, 200] };
636
584
  }
637
585
 
638
586
  const { idPhotoUri, selfiePhotoUri } = params;
@@ -640,7 +588,11 @@ export class KYCService {
640
588
  await appendFileToFormData(formData, 'id_photo', idPhotoUri, 'id_card_photo.jpg', 'image/jpeg');
641
589
  await appendFileToFormData(formData, 'selfie_photo', selfiePhotoUri, 'selfie_final.jpg', 'image/jpeg');
642
590
 
643
- const res = await axios.post(`${this.faceServiceURL}/recognize_face/`, formData, { timeout: 45000 });
591
+ const res = await axios.post(`${this.faceServiceURL}/recognize_face/`, formData, {
592
+ // Let Axios determine Content-Type boundary natively
593
+ timeout: 45000
594
+ });
595
+
644
596
  if (res.data?.detail) throw new Error(res.data.detail);
645
597
  return res.data;
646
598
  }
@@ -672,8 +624,7 @@ export class KYCService {
672
624
  "status": "PENDING",
673
625
  "metadata": {},
674
626
  }
675
- const res = await axios.post<SessionResponse>(`${KYCConfig.getBackendUrl()}/verification/sessions/`,
676
- data, {
627
+ const res = await axios.post<SessionResponse>(`${KYCConfig.getBackendUrl()}/verification/sessions/`, data, {
677
628
  headers: {
678
629
  'Content-Type': 'application/json',
679
630
  ...(apiKey ? { 'Authorization': `ApiKey ${apiKey}` } : { 'Authorization': `Bearer ${token}` })
@@ -682,146 +633,112 @@ export class KYCService {
682
633
  return res.data;
683
634
  } catch (error: any) {
684
635
  logger.error('Error creating session:', JSON.stringify(errorMessage(error), null, 2));
685
-
686
- // Extract backend error message if available
687
636
  const backendMessage = errorMessage(error);
688
-
689
637
  if (backendMessage) {
690
638
  logger.error('Backend error message:', JSON.stringify(truncateFields(backendMessage), null, 2));
691
639
  throw new Error(`Backend error: ${JSON.stringify(truncateFields(backendMessage))}`);
692
640
  }
693
-
694
641
  throw error;
695
642
  }
696
643
  }
697
644
 
698
-
699
645
  async verificationSession(payload: VerificationSessionRequest): Promise<any> {
700
- try {
701
- const { session_id, step, data, templateId, action, apiKey } = payload;
702
- const token = apiKey ? undefined : await authentification();
703
-
704
- if (!apiKey && !token) {
705
- throw new Error("Authentication failed: No API key or Token available.");
706
- }
707
-
708
- // 1. Original Shallow Clone (Preserves file paths perfectly)
709
- const normalizedData = { ...data };
710
-
711
- logger.log("🔍 [DocType Check] Raw incoming data.metadata.document_type:", normalizedData?.metadata?.document_type);
712
- logger.log("🔍 [DocType Check] Raw incoming data.documentType:", normalizedData?.documentType);
713
-
714
- // Enhanced fallback to guarantee we find the document type
715
- const rawDocType =
716
- normalizedData?.metadata?.document_type ||
717
- normalizedData?.documentType ||
718
- normalizedData?.documents?.documentType;
719
-
720
- if (rawDocType) {
721
- const originalDocType = rawDocType;
722
- const mappedDocType = documentTypeMap[originalDocType] ?? originalDocType;
723
-
724
- normalizedData.metadata = {
725
- ...normalizedData.metadata,
726
- document_type: mappedDocType,
727
- };
646
+ try {
647
+ const { session_id, step, data, templateId, action, apiKey } = payload;
648
+ const token = apiKey ? undefined : await authentification();
649
+ if (!apiKey && !token) {
650
+ throw new Error("Authentication failed: No API key or Token available.");
651
+ }
728
652
 
729
- normalizedData.documentType = mappedDocType;
730
-
731
- if (normalizedData.documents) {
732
- normalizedData.documents = {
733
- ...normalizedData.documents,
734
- documentType: mappedDocType
653
+ const normalizedData = { ...data };
654
+ logger.log("🔍 [DocType Check] Raw incoming data.metadata.document_type:", normalizedData?.metadata?.document_type);
655
+ logger.log("🔍 [DocType Check] Raw incoming data.documentType:", normalizedData?.documentType);
656
+
657
+ const rawDocType = normalizedData?.metadata?.document_type || normalizedData?.documentType || normalizedData?.documents?.documentType;
658
+
659
+ if (rawDocType) {
660
+ const originalDocType = rawDocType;
661
+ const mappedDocType = documentTypeMap[originalDocType] ?? originalDocType;
662
+ normalizedData.metadata = {
663
+ ...normalizedData.metadata,
664
+ document_type: mappedDocType,
735
665
  };
736
-
737
- const isSingleSided = ['passport', 'visa'].includes(mappedDocType.toLowerCase());
738
- if (isSingleSided && !normalizedData.documents.back) {
739
- normalizedData.documents.back = {};
740
- }
741
-
742
- if (normalizedData.documents.front?.mrz && typeof normalizedData.documents.front.mrz === 'string') {
743
- try {
744
- normalizedData.documents.front = {
745
- ...normalizedData.documents.front,
746
- mrz: JSON.parse(normalizedData.documents.front.mrz)
747
- };
748
- } catch (e) {
749
- logger.log("Warning: Could not parse front MRZ JSON");
666
+ normalizedData.documentType = mappedDocType;
667
+ if (normalizedData.documents) {
668
+ normalizedData.documents = { ...normalizedData.documents, documentType: mappedDocType };
669
+ const isSingleSided = ['passport', 'visa'].includes(mappedDocType.toLowerCase());
670
+ if (isSingleSided && !normalizedData.documents.back) {
671
+ normalizedData.documents.back = {};
750
672
  }
751
- }
752
-
753
- if (normalizedData.documents.back?.mrz && typeof normalizedData.documents.back.mrz === 'string') {
754
- try {
755
- normalizedData.documents.back = {
756
- ...normalizedData.documents.back,
757
- mrz: JSON.parse(normalizedData.documents.back.mrz)
758
- };
759
- } catch (e) {
760
- logger.log("Warning: Could not parse back MRZ JSON");
673
+ if (normalizedData.documents.front?.mrz && typeof normalizedData.documents.front.mrz === 'string') {
674
+ try {
675
+ normalizedData.documents.front = { ...normalizedData.documents.front, mrz: JSON.parse(normalizedData.documents.front.mrz) };
676
+ } catch (e) {
677
+ logger.log("Warning: Could not parse front MRZ JSON");
678
+ }
679
+ }
680
+ if (normalizedData.documents.back?.mrz && typeof normalizedData.documents.back.mrz === 'string') {
681
+ try {
682
+ normalizedData.documents.back = { ...normalizedData.documents.back, mrz: JSON.parse(normalizedData.documents.back.mrz) };
683
+ } catch (e) {
684
+ logger.log("Warning: Could not parse back MRZ JSON");
685
+ }
761
686
  }
762
687
  }
763
688
  }
764
- }
765
-
766
- const payloadData = {
767
- action,
768
- data: {
769
- ...normalizedData,
770
- step,
771
- templateId,
772
- },
773
- session_id,
774
- timestamp: new Date().toISOString(),
775
- };
776
689
 
777
- const url = `${KYCConfig.getBackendUrl()}/verification/api/kyc/sessions/${session_id}/steps/${step}/`;
778
- const logPayload = truncateFields({ payloadData, session_id, step });
779
-
780
- logger.log(
781
- "verificationSession payload",
782
- JSON.stringify(
783
- {
784
- url,
785
- apiKey: apiKey ? "***HIDDEN***" : "None",
786
- token: token ? "***HIDDEN***" : "None",
787
- payload: logPayload,
690
+ const payloadData = {
691
+ action,
692
+ data: {
693
+ ...normalizedData,
694
+ step,
695
+ templateId,
788
696
  },
789
- null,
790
- 2
791
- )
792
- );
793
-
794
- const headers: Record<string, string> = {
795
- "Content-Type": "application/json",
796
- ...(apiKey ? { Authorization: `ApiKey ${apiKey}` } : {}),
797
- ...(token && !apiKey ? { Authorization: `Bearer ${token}` } : {})
798
- };
799
-
800
- const res = await axios.post<SessionResponse>(url, payloadData, { headers });
697
+ session_id,
698
+ timestamp: new Date().toISOString(),
699
+ };
700
+ const url = `${KYCConfig.getBackendUrl()}/verification/api/kyc/sessions/${session_id}/steps/${step}/`;
701
+ const logPayload = truncateFields({ payloadData, session_id, step });
702
+
703
+ logger.log(
704
+ "verificationSession payload",
705
+ JSON.stringify(
706
+ {
707
+ url,
708
+ apiKey: apiKey ? "***HIDDEN***" : "None",
709
+ token: token ? "***HIDDEN***" : "None",
710
+ payload: logPayload,
711
+ },
712
+ null,
713
+ 2
714
+ )
715
+ );
801
716
 
802
- logger.log(
803
- "verificationSession res",
804
- JSON.stringify(truncateFields(res.data), null, 2)
805
- );
806
- return res.data;
717
+ const headers: Record<string, string> = {
718
+ "Content-Type": "application/json",
719
+ ...(apiKey ? { Authorization: `ApiKey ${apiKey}` } : {}),
720
+ ...(token && !apiKey ? { Authorization: `Bearer ${token}` } : {})
721
+ };
807
722
 
808
- } catch (error: any) {
809
- const parsedError = errorMessage(error);
810
- const finalErrorMessage = parsedError || error?.message || "Unknown validation error";
811
-
812
- logger.error(
813
- "Error validating component:",
814
- JSON.stringify(finalErrorMessage, null, 2)
815
- );
816
- throw new Error(typeof finalErrorMessage === 'string' ? finalErrorMessage : JSON.stringify(finalErrorMessage));
723
+ const res = await axios.post<SessionResponse>(url, payloadData, { headers });
724
+
725
+ logger.log(
726
+ "verificationSession res",
727
+ JSON.stringify(truncateFields(res.data), null, 2)
728
+ );
729
+ return res.data;
730
+ } catch (error: any) {
731
+ const parsedError = errorMessage(error);
732
+ const finalErrorMessage = parsedError || error?.message || "Unknown validation error";
733
+ logger.error(
734
+ "Error validating component:",
735
+ JSON.stringify(finalErrorMessage, null, 2)
736
+ );
737
+ throw new Error(typeof finalErrorMessage === 'string' ? finalErrorMessage : JSON.stringify(finalErrorMessage));
738
+ }
817
739
  }
818
- }
819
740
 
820
- /** Send email verification code. POST /api/v1/accounts/email/send/ */
821
- async sendEmailVerificationCode(
822
- email: string,
823
- auth?: { apiKey?: string; token?: string }
824
- ): Promise<unknown> {
741
+ async sendEmailVerificationCode(email: string, auth?: { apiKey?: string; token?: string }): Promise<unknown> {
825
742
  const url = `${KYCConfig.getBackendUrl()}/accounts/email/send/`;
826
743
  const token = auth?.apiKey ? undefined : (auth?.token ?? await authentification());
827
744
  const res = await axios.post(
@@ -837,11 +754,7 @@ export class KYCService {
837
754
  return res.data;
838
755
  }
839
756
 
840
- /** Verify email with OTP. POST /api/v1/accounts/email/verify/ */
841
- async verifyEmailCode(
842
- otp: string,
843
- auth?: { apiKey?: string; token?: string }
844
- ): Promise<unknown> {
757
+ async verifyEmailCode(otp: string, auth?: { apiKey?: string; token?: string }): Promise<unknown> {
845
758
  const url = `${KYCConfig.getBackendUrl()}/accounts/email/verify/`;
846
759
  const token = auth?.apiKey ? undefined : (auth?.token ?? await authentification());
847
760
  const res = await axios.post(
@@ -859,231 +772,168 @@ export class KYCService {
859
772
 
860
773
  async getVerificationResult(session_id: string, apiKey?: string): Promise<any> {
861
774
  try {
862
-
863
775
  const token = apiKey ? undefined : await authentification();
864
-
865
776
  const url = `${KYCConfig.getBackendUrl()}/verification/api/kyc/result/?session_id=${session_id}`;
866
-
867
777
  const controller = new AbortController();
868
778
  const timeoutId = setTimeout(() => {
869
779
  logger.log("⏳ 8 seconds passed. Force-killing hanging Axios request...");
870
780
  controller.abort();
871
781
  }, 8000);
872
-
782
+
873
783
  const headers: Record<string, string> = {
874
784
  'Content-Type': 'application/json',
875
785
  };
876
-
877
786
  if (apiKey) {
878
787
  headers["Authorization"] = `ApiKey ${apiKey}`;
879
788
  } else if (token) {
880
789
  headers["Authorization"] = `Bearer ${token}`;
881
790
  }
882
-
883
791
  logger.log("3. Firing Axios GET to:", url);
884
-
885
- const res = await axios.get(url, {
886
- headers,
887
- signal: controller.signal
888
- });
889
-
890
- // Clear the timeout if the request succeeds!
792
+
793
+ const res = await axios.get(url, { headers, signal: controller.signal });
891
794
  clearTimeout(timeoutId);
892
795
 
893
796
  return res.data;
894
-
895
797
  } catch (error: any) {
896
- // Catch the AbortController kill signal and format it as a timeout
897
798
  if (axios.isCancel(error) || error.name === 'AbortError' || error.name === 'CanceledError') {
898
799
  logger.error('Axios request was forcefully aborted due to timeout');
899
800
  throw new Error('Timeout exceeded');
900
801
  }
901
-
902
802
  logger.error('Error getting verification result:', JSON.stringify(errorMessage(error) || error.message, null, 2));
903
803
  throw error;
904
804
  }
905
805
  }
906
806
 
907
- /** Send WhatsApp verification code. POST /api/v1/accounts/send-whatsapp-verification/ */
908
807
  async sendWhatsAppVerificationCode(
909
808
  sessionId: string,
910
809
  phoneNumber: string,
911
810
  auth?: { apiKey?: string; token?: string }
912
811
  ): Promise<unknown> {
913
812
  const url = `${KYCConfig.getBackendUrl()}/accounts/send-whatsapp-verification/`;
914
-
915
- // 1. Formatting (Ensure consistency with verification step)
916
813
  const formattedPhoneNumber = phoneNumber.replace(/^\+/, '');
917
-
918
- // 2. Prepare Payload
919
- const payload = {
920
- session_id: sessionId,
921
- phone_number: formattedPhoneNumber
922
- };
923
-
924
-
925
-
814
+ const payload = { session_id: sessionId, phone_number: formattedPhoneNumber };
926
815
  const token = auth?.apiKey ? undefined : (auth?.token ?? await authentification());
816
+
927
817
  const headers = {
928
- 'Content-Type': 'application/json',
929
- ...(auth?.apiKey ? { 'Authorization': `ApiKey ${auth.apiKey}` } : { 'Authorization': `Bearer ${token}` }),
818
+ 'Content-Type': 'application/json',
819
+ ...(auth?.apiKey ? { 'Authorization': `ApiKey ${auth.apiKey}` } : { 'Authorization': `Bearer ${token}` }),
930
820
  };
931
-
932
-
933
-
934
821
  try {
935
- const res = await axios.post(url, payload, { headers });
936
-
937
- logger.log("✅ WhatsApp Send Success:", res.data);
938
- return res.data;
939
-
822
+ const res = await axios.post(url, payload, { headers });
823
+ logger.log("✅ WhatsApp Send Success:", res.data);
824
+ return res.data;
940
825
  } catch (error: any) {
941
- if (error.response) {
942
- logger.error("🛑 WhatsApp Send Failed (Server Response):", {
943
- status: error.response.status,
944
- data: error.response.data,
945
- headers: error.response.headers
946
- });
947
- }
948
- throw error;
826
+ if (error.response) {
827
+ logger.error("🛑 WhatsApp Send Failed (Server Response):", {
828
+ status: error.response.status,
829
+ data: error.response.data,
830
+ headers: error.response.headers
831
+ });
832
+ }
833
+ throw error;
949
834
  }
950
835
  }
951
836
 
952
837
  async verifyWhatsAppCode(
953
838
  sessionId: string,
954
839
  otp: string,
955
- phoneNumber: string,
840
+ phoneNumber: string,
956
841
  auth?: { apiKey?: string; token?: string }
957
842
  ): Promise<unknown> {
958
- const url = `${KYCConfig.getBackendUrl()}/accounts/verify-whatsapp-code/`;
959
-
843
+ const url = `${KYCConfig.getBackendUrl()}/accounts/verify-whatsapp-code/`;
960
844
  const formattedPhoneNumber = phoneNumber.replace(/^\+/, '');
961
-
962
- const payload = {
963
- session_id: sessionId,
964
- verification_code: otp,
965
- phone_number: formattedPhoneNumber
966
- };
967
-
968
-
969
-
845
+ const payload = { session_id: sessionId, verification_code: otp, phone_number: formattedPhoneNumber };
970
846
  const token = auth?.apiKey ? undefined : (auth?.token ?? await authentification());
847
+
971
848
  const headers = {
972
- 'Content-Type': 'application/json',
973
- ...(auth?.apiKey ? { 'Authorization': `ApiKey ${auth.apiKey}` } : { 'Authorization': `Bearer ${token}` }),
849
+ 'Content-Type': 'application/json',
850
+ ...(auth?.apiKey ? { 'Authorization': `ApiKey ${auth.apiKey}` } : { 'Authorization': `Bearer ${token}` }),
974
851
  };
975
-
976
-
977
852
  try {
978
- const res = await axios.post(url, payload, { headers });
979
-
980
- return res.data;
981
-
853
+ const res = await axios.post(url, payload, { headers });
854
+ return res.data;
982
855
  } catch (error: any) {
983
- if (error.response) {
984
- logger.error("🛑 WhatsApp Verification Failed (Server Response):", {
985
- status: error.response.status,
986
- data: error.response.data,
987
- headers: error.response.headers
988
- });
989
- }
990
- throw error;
856
+ if (error.response) {
857
+ logger.error("🛑 WhatsApp Verification Failed (Server Response):", {
858
+ status: error.response.status,
859
+ data: error.response.data,
860
+ headers: error.response.headers
861
+ });
862
+ }
863
+ throw error;
991
864
  }
992
- }
993
-
994
- // Inside KYCService.ts
995
-
996
- async validateBackTemplate(params: {
997
- region: string;
998
- docType: string;
999
- frontTemplateName: string;
1000
- backTemplateName: string;
1001
- subRegion: string;
1002
- token: string;
1003
- }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
1004
- if (env === 'SANDBOX') {
1005
- logger.log("SANDBOX mode: Skipping back template validation");
1006
- return { is_match: true, status_message: "SANDBOX: Valid Match" };
1007
865
  }
1008
866
 
1009
- const { region, docType, frontTemplateName, backTemplateName, subRegion, token } = params;
1010
-
1011
- const queryParams = new URLSearchParams({
1012
- region: region,
1013
- doc_type: docType,
1014
- front_template_name: frontTemplateName,
1015
- back_template_name: backTemplateName,
1016
- sub_region: subRegion || ""
1017
- }).toString();
1018
-
1019
- const url = `${this.mrzServiceURL}/validate_back_template/?${queryParams}`;
1020
-
1021
- logger.log('validateBackTemplate URL', url);
1022
-
1023
- try {
1024
- // Pass `null` for the body since the data is now in the URL
1025
- const res = await axios.post(url, null, {
1026
- headers: {
1027
- 'Accept': 'application/json',
1028
- 'Authorization': `Bearer ${token}`
1029
- },
1030
- timeout: 20000,
1031
- });
1032
- logger.log('validateBackTemplate res', JSON.stringify(res.data, null, 2));
1033
- return res.data;
1034
- } catch (e: any) {
1035
- const errorMsg = e.response?.data?.detail || e.response?.data?.Error || e.message;
1036
- logger.error('Error validating back template:', JSON.stringify(errorMsg));
1037
- throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Template validation failed');
867
+ async validateBackTemplate(params: {
868
+ region: string;
869
+ docType: string;
870
+ frontTemplateName: string;
871
+ backTemplateName: string;
872
+ subRegion: string;
873
+ token: string;
874
+ }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
875
+ if (env === 'SANDBOX') {
876
+ logger.log("SANDBOX mode: Skipping back template validation");
877
+ return { is_match: true, status_message: "SANDBOX: Valid Match" };
878
+ }
879
+ const { region, docType, frontTemplateName, backTemplateName, subRegion, token } = params;
880
+ const queryParams = new URLSearchParams({
881
+ region: region,
882
+ doc_type: docType,
883
+ front_template_name: frontTemplateName,
884
+ back_template_name: backTemplateName,
885
+ sub_region: subRegion || ""
886
+ }).toString();
887
+ const url = `${this.mrzServiceURL}/validate_back_template/?${queryParams}`;
888
+ logger.log('validateBackTemplate URL', url);
889
+
890
+ try {
891
+ const res = await axios.post(url, null, {
892
+ headers: {
893
+ 'Accept': 'application/json',
894
+ 'Authorization': `Bearer ${token}`
895
+ },
896
+ timeout: 20000,
897
+ });
898
+ logger.log('validateBackTemplate res', JSON.stringify(res.data, null, 2));
899
+ return res.data;
900
+ } catch (e: any) {
901
+ const errorMsg = e.response?.data?.detail || e.response?.data?.Error || e.message;
902
+ logger.error('Error validating back template:', JSON.stringify(errorMsg));
903
+ throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Template validation failed');
904
+ }
1038
905
  }
1039
- }
1040
906
 
1041
- /**
1042
- * Generates a new referral code for a specific session.
1043
- * Endpoint: POST /api/v1/core/referral-code/generate/
1044
- */
1045
- async generateReferralCode(
1046
- payload: { session_id: string; [key: string]: any },
1047
- apiKey: string // 🚨 Now strictly required
1048
- ): Promise<any> {
1049
- try {
1050
- logger.log(`Generating referral code for session: ${payload.session_id}`);
1051
-
1052
- if (!apiKey) {
1053
- throw new Error("Authentication failed: API key is required.");
907
+ async generateReferralCode(
908
+ payload: { session_id: string; [key: string]: any },
909
+ apiKey: string
910
+ ): Promise<any> {
911
+ try {
912
+ logger.log(`Generating referral code for session: ${payload.session_id}`);
913
+ if (!apiKey) {
914
+ throw new Error("Authentication failed: API key is required.");
915
+ }
916
+ const url = `${KYCConfig.getBackendUrl()}/core/referral-code/generate/`;
917
+ const headers: Record<string, string> = {
918
+ "Content-Type": "application/json",
919
+ "Authorization": `ApiKey ${apiKey}`
920
+ };
921
+
922
+ const res = await axios.post(url, payload, { headers, timeout: 8000 });
923
+ logger.log("generateReferralCode SUCCESS:", JSON.stringify(res.data, null, 2));
924
+ return res.data;
925
+ } catch (error: any) {
926
+ const parsedError = errorMessage(error);
927
+ const finalErrorMessage = parsedError || error?.message || "Failed to generate referral code";
928
+ logger.error("Error generating referral code:", typeof finalErrorMessage === 'string' ? finalErrorMessage : JSON.stringify(finalErrorMessage));
929
+ throw new Error(typeof finalErrorMessage === 'string' ? finalErrorMessage : JSON.stringify(finalErrorMessage));
1054
930
  }
1055
-
1056
- const url = `${KYCConfig.getBackendUrl()}/core/referral-code/generate/`;
1057
-
1058
- const headers: Record<string, string> = {
1059
- "Content-Type": "application/json",
1060
- "Authorization": `ApiKey ${apiKey}` // 🚨 Strictly using ApiKey
1061
- };
1062
-
1063
- const res = await axios.post(url, payload, {
1064
- headers,
1065
- timeout: 8000
1066
- });
1067
-
1068
- logger.log("generateReferralCode SUCCESS:", JSON.stringify(res.data, null, 2));
1069
- return res.data;
1070
-
1071
- } catch (error: any) {
1072
- const parsedError = errorMessage(error);
1073
- const finalErrorMessage = parsedError || error?.message || "Failed to generate referral code";
1074
- logger.error("Error generating referral code:", typeof finalErrorMessage === 'string' ? finalErrorMessage : JSON.stringify(finalErrorMessage));
1075
- throw new Error(typeof finalErrorMessage === 'string' ? finalErrorMessage : JSON.stringify(finalErrorMessage));
1076
931
  }
1077
932
  }
1078
933
 
1079
-
1080
-
1081
- }
1082
-
1083
934
  const kycService = new KYCService("", "");
1084
935
  export default kycService;
1085
936
 
1086
- // Pour éviter d'afficher des champs trop longs, on tronque les valeurs longues dans le log
1087
937
  export function truncateFields(obj: any, maxLength = 420): any {
1088
938
  if (typeof obj !== 'object' || obj === null) return obj;
1089
939
  if (Array.isArray(obj)) return obj.map(item => truncateFields(item, maxLength));
@@ -1102,28 +952,18 @@ export function truncateFields(obj: any, maxLength = 420): any {
1102
952
  return truncated;
1103
953
  }
1104
954
 
1105
-
1106
-
1107
-
1108
- // Strictly define the return type as Promise<string>
1109
955
  export const authentification = async (): Promise<string> => {
1110
956
  try {
1111
- // 1. If we have a valid token (with 10 seconds of breathing room), return it immediately
1112
957
  if (cachedToken && tokenExpiryTime && Date.now() < tokenExpiryTime - 10000) {
1113
958
  return cachedToken;
1114
959
  }
1115
-
1116
- // 2. RACE CONDITION PREVENTION
1117
960
  if (activeAuthRequest) {
1118
961
  return activeAuthRequest;
1119
962
  }
1120
-
1121
- // 3. Create the request and lock it
963
+
1122
964
  activeAuthRequest = (async () => {
1123
965
  logger.log('Authenticating: Fetching new Keycloak token...');
1124
-
1125
966
  const params = 'client_id=kyc_frontend&client_secret=QhgAmvKgmwODzsEp98dnA4PeUEMMaFHd&grant_type=client_credentials';
1126
-
1127
967
  const res = await axios.post(
1128
968
  `https://idms.sanctumkey.com/realms/kyc/protocol/openid-connect/token`,
1129
969
  params,
@@ -1133,44 +973,27 @@ export const authentification = async (): Promise<string> => {
1133
973
  },
1134
974
  }
1135
975
  );
1136
-
1137
976
  cachedToken = res.data.access_token;
1138
-
1139
977
  if (!cachedToken) {
1140
978
  throw new Error("Keycloak returned a success response but no access_token was found.");
1141
979
  }
1142
-
1143
- // Calculate exact expiry timestamp (expires_in is usually in seconds)
1144
- const expiresInMs = (res.data.expires_in || 300) * 1000;
980
+ const expiresInMs = (res.data.expires_in || 300) * 1000;
1145
981
  tokenExpiryTime = Date.now() + expiresInMs;
1146
-
1147
- return cachedToken;
982
+ return cachedToken;
1148
983
  })();
1149
-
984
+
1150
985
  const finalToken = await activeAuthRequest;
1151
986
  return finalToken;
1152
-
1153
987
  } catch (error: any) {
1154
- cachedToken = null; // Clear bad cache
1155
-
1156
- const safeErrorMessage =
1157
- error?.response?.data?.error_description ||
1158
- error?.response?.data?.error ||
1159
- error?.message ||
1160
- 'Unknown network error';
1161
-
988
+ cachedToken = null;
989
+ const safeErrorMessage = error?.response?.data?.error_description || error?.response?.data?.error || error?.message || 'Unknown network error';
1162
990
  logger.error('Error authentifying:', safeErrorMessage);
1163
991
  throw new Error(`Échec de l'authentification: ${safeErrorMessage}`);
1164
-
1165
992
  } finally {
1166
- // 4. Always clear the lock when the request finishes
1167
993
  activeAuthRequest = null;
1168
994
  }
1169
995
  }
1170
996
 
1171
997
  export const errorMessage = (error: any) => {
1172
- return error.response?.data?.message ||
1173
- error.response?.data?.detail ||
1174
- error.response?.data?.error ||
1175
- error.response?.data;
998
+ return error.response?.data?.message || error.response?.data?.detail || error.response?.data?.error || error.response?.data;
1176
999
  }