@sanctum-key/react-native-sdk 1.0.33 → 1.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/package.json +1 -1
- package/build/src/components/EnhancedCameraView.d.ts.map +1 -1
- package/build/src/components/EnhancedCameraView.js +82 -28
- package/build/src/components/EnhancedCameraView.js.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +84 -33
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/modules/api/KYCService.d.ts +10 -18
- package/build/src/modules/api/KYCService.d.ts.map +1 -1
- package/build/src/modules/api/KYCService.js +183 -588
- package/build/src/modules/api/KYCService.js.map +1 -1
- package/package.json +1 -1
- package/src/components/EnhancedCameraView.tsx +92 -33
- package/src/components/KYCElements/IDCardCapture.tsx +116 -53
- package/src/modules/api/KYCService.ts +255 -938
|
@@ -13,6 +13,7 @@ const appendFileToFormData = async (formData: FormData, key: string, uri: string
|
|
|
13
13
|
const blob = await response.blob();
|
|
14
14
|
formData.append(key, blob, name);
|
|
15
15
|
} else {
|
|
16
|
+
// React Native Native structure
|
|
16
17
|
formData.append(key, { uri, type, name } as any);
|
|
17
18
|
}
|
|
18
19
|
};
|
|
@@ -23,59 +24,27 @@ const documentTypeMap: Record<string, string> = {
|
|
|
23
24
|
healthInsuranceCard: "health_insurance_card",
|
|
24
25
|
residencePermit: "permanent_residence_card",
|
|
25
26
|
identityCard: "identity_card",
|
|
26
|
-
|
|
27
|
-
nationalId: "identity_card", // We saw this in your earlier logs!
|
|
27
|
+
nationalId: "identity_card",
|
|
28
28
|
national_id: "identity_card",
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
permanent_residence: "permanent_residence_card", // Missing the word 'card'
|
|
29
|
+
drivers_licence: "driving_licence",
|
|
30
|
+
drivers_license: "driving_licence",
|
|
31
|
+
driving_license: "driving_licence",
|
|
32
|
+
permanent_residence: "permanent_residence_card",
|
|
34
33
|
};
|
|
35
34
|
|
|
36
35
|
let cachedToken: string | null = null;
|
|
37
36
|
let tokenExpiryTime: number | null = null;
|
|
38
|
-
let activeAuthRequest: Promise<string> | null = null;
|
|
39
|
-
|
|
40
|
-
export interface KYCRequest {
|
|
41
|
-
userId: string;
|
|
42
|
-
documentImage: string;
|
|
43
|
-
selfieImage: string;
|
|
44
|
-
documentType: 'passport' | 'id_card' | 'drivers_license';
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface KYCResponse {
|
|
48
|
-
success: boolean;
|
|
49
|
-
verificationId: string;
|
|
50
|
-
status: 'pending' | 'approved' | 'rejected';
|
|
51
|
-
message?: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface FaceDetectionRequest {
|
|
55
|
-
selfieImage: string;
|
|
56
|
-
documentImage: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface FaceDetectionResponse {
|
|
60
|
-
match: boolean;
|
|
61
|
-
confidence: number;
|
|
62
|
-
message: string;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface SelfieVideoResponse {
|
|
66
|
-
orientation_direction: "center" | "left" | "right",
|
|
67
|
-
turn_score: number;
|
|
68
|
-
bbox: number[];
|
|
69
|
-
capture: boolean;
|
|
70
|
-
instruction: string;
|
|
71
|
-
error: string;
|
|
72
|
-
}
|
|
37
|
+
let activeAuthRequest: Promise<string> | null = null;
|
|
73
38
|
|
|
39
|
+
export interface KYCRequest { userId: string; documentImage: string; selfieImage: string; documentType: 'passport' | 'id_card' | 'drivers_license'; }
|
|
40
|
+
export interface KYCResponse { success: boolean; verificationId: string; status: 'pending' | 'approved' | 'rejected'; message?: string; }
|
|
41
|
+
export interface FaceDetectionRequest { selfieImage: string; documentImage: string; }
|
|
42
|
+
export interface FaceDetectionResponse { match: boolean; confidence: number; message: string; }
|
|
43
|
+
export interface SelfieVideoResponse { orientation_direction: "center" | "left" | "right", turn_score: number; bbox: number[]; capture: boolean; instruction: string; error: string; }
|
|
74
44
|
|
|
75
45
|
export class KYCService {
|
|
76
46
|
private baseURL: string;
|
|
77
47
|
private apiKey: string;
|
|
78
|
-
// Additional service base URLs (fixed as per current infra)
|
|
79
48
|
private faceServiceURL = 'https://face-infera.sanctumkey.com';
|
|
80
49
|
private textExtractionServiceURL = 'https://text-infera.sanctumkey.com';
|
|
81
50
|
private mrzServiceURL = 'https://mrz-infera.sanctumkey.com';
|
|
@@ -94,734 +63,286 @@ export class KYCService {
|
|
|
94
63
|
};
|
|
95
64
|
}
|
|
96
65
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
throw error;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
66
|
+
// 🚨 BULLETPROOF MULTIPART FETCHER
|
|
67
|
+
// Replaces Axios for file uploads to guarantee iOS compatibility
|
|
68
|
+
private async crossPlatformMultipartUpload<T>(url: string, formData: FormData, token?: string, timeoutMs: number = 60000): Promise<T> {
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
71
|
+
|
|
72
|
+
const headers: Record<string, string> = {
|
|
73
|
+
'Accept': 'application/json',
|
|
74
|
+
};
|
|
75
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
110
76
|
|
|
111
|
-
async detectFace(data: FaceDetectionRequest): Promise<FaceDetectionResponse> {
|
|
112
77
|
try {
|
|
113
|
-
const response = await
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
78
|
+
const response = await fetch(url, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers, // Do NOT set Content-Type, native fetch sets the boundary
|
|
81
|
+
body: formData,
|
|
82
|
+
signal: controller.signal
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
clearTimeout(timeoutId);
|
|
86
|
+
|
|
87
|
+
const responseText = await response.text();
|
|
88
|
+
let responseData;
|
|
89
|
+
try {
|
|
90
|
+
responseData = responseText ? JSON.parse(responseText) : {};
|
|
91
|
+
} catch (e) {
|
|
92
|
+
responseData = { detail: responseText };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
// Mock Axios error structure so your `errorMessage` helper still works!
|
|
97
|
+
throw { response: { status: response.status, data: responseData } };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return responseData as T;
|
|
101
|
+
} catch (error: any) {
|
|
102
|
+
clearTimeout(timeoutId);
|
|
103
|
+
if (error.name === 'AbortError') {
|
|
104
|
+
throw { response: { data: { message: 'Request timed out' } } };
|
|
105
|
+
}
|
|
121
106
|
throw error;
|
|
122
107
|
}
|
|
123
108
|
}
|
|
124
109
|
|
|
125
|
-
async
|
|
110
|
+
async submitKYC(data: KYCRequest): Promise<KYCResponse> {
|
|
126
111
|
try {
|
|
127
|
-
const response = await axios.post(
|
|
128
|
-
`${this.baseURL}/ai/document-validation`,
|
|
129
|
-
{ documentImage, documentType },
|
|
130
|
-
{ headers: this.getHeaders() }
|
|
131
|
-
);
|
|
112
|
+
const response = await axios.post(`${this.baseURL}/kyc/submit`, data, { headers: this.getHeaders() });
|
|
132
113
|
return response.data;
|
|
133
|
-
} catch (error) {
|
|
134
|
-
logger.error('Error validating document:', error);
|
|
135
|
-
throw error;
|
|
136
|
-
}
|
|
114
|
+
} catch (error) { throw error; }
|
|
137
115
|
}
|
|
138
116
|
|
|
139
|
-
async
|
|
117
|
+
async detectFace(data: FaceDetectionRequest): Promise<FaceDetectionResponse> {
|
|
140
118
|
try {
|
|
141
|
-
const response = await axios.
|
|
142
|
-
`${this.baseURL}/kyc/status/${verificationId}`,
|
|
143
|
-
{ headers: this.getHeaders() }
|
|
144
|
-
);
|
|
119
|
+
const response = await axios.post(`${this.baseURL}/ai/face-detection`, data, { headers: this.getHeaders() });
|
|
145
120
|
return response.data;
|
|
146
|
-
} catch (error) {
|
|
147
|
-
logger.error('Error getting KYC status:', error);
|
|
148
|
-
throw error;
|
|
149
|
-
}
|
|
121
|
+
} catch (error) { throw error; }
|
|
150
122
|
}
|
|
151
123
|
|
|
152
124
|
async processSelfieOrientationPicture(videoFile: string, token: string): Promise<SelfieVideoResponse[]> {
|
|
153
|
-
|
|
154
125
|
try {
|
|
155
126
|
const formData = new FormData();
|
|
156
127
|
await appendFileToFormData(formData, 'file', videoFile, 'selfie_picture.jpg', 'image/jpeg');
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
`${this.faceServiceURL}/detect_face_orientation/`,
|
|
160
|
-
formData,
|
|
161
|
-
{
|
|
162
|
-
timeout: 20000,
|
|
163
|
-
headers: { 'Content-Type': 'multipart/form-data', 'Authorization': `Bearer ${token}`, },
|
|
164
|
-
}
|
|
128
|
+
|
|
129
|
+
const data = await this.crossPlatformMultipartUpload<SelfieVideoResponse[]>(
|
|
130
|
+
`${this.faceServiceURL}/detect_face_orientation/`, formData, token, 20000
|
|
165
131
|
);
|
|
166
|
-
return
|
|
167
|
-
} catch (error) {
|
|
168
|
-
logger.error('Error processing selfie orientation:', error);
|
|
169
|
-
throw error;
|
|
170
|
-
}
|
|
132
|
+
return data;
|
|
133
|
+
} catch (error) { throw error; }
|
|
171
134
|
}
|
|
172
135
|
|
|
173
136
|
async processOrientationVideo(videoFile: string, env: KycEnvironment = 'PRODUCTION'): Promise<OrientationVideoResponse> {
|
|
174
137
|
try {
|
|
175
|
-
// SANDBOX mode: skip AI verification and return mock response
|
|
176
138
|
if (env === 'SANDBOX') {
|
|
177
|
-
|
|
178
|
-
logger.log("SANDBOX mode: Returning mock orientation video response");
|
|
179
|
-
return {
|
|
180
|
-
success: true,
|
|
181
|
-
data: {
|
|
182
|
-
center: { captured: true, frame: 10 },
|
|
183
|
-
left: { captured: true, frame: 30 },
|
|
184
|
-
right: { captured: true, frame: 50 }
|
|
185
|
-
},
|
|
186
|
-
message: 'SANDBOX: Orientation video processed successfully (mocked)'
|
|
187
|
-
};
|
|
139
|
+
return { success: true, data: { center: { captured: true, frame: 10 }, left: { captured: true, frame: 30 }, right: { captured: true, frame: 50 } }, message: 'SANDBOX Mock' };
|
|
188
140
|
}
|
|
189
|
-
|
|
190
|
-
// Create FormData for multipart/form-data request
|
|
191
141
|
const formData = new FormData();
|
|
192
142
|
await appendFileToFormData(formData, 'file', videoFile, 'orientation_video.mp4', 'video/mp4');
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
`${this.orientationServiceURL}/process_orientation_video_stream/`,
|
|
197
|
-
formData,
|
|
198
|
-
{
|
|
199
|
-
// Let axios set the proper multipart boundary header automatically
|
|
200
|
-
timeout: 90000,
|
|
201
|
-
}
|
|
143
|
+
|
|
144
|
+
const data = await this.crossPlatformMultipartUpload<any>(
|
|
145
|
+
`${this.orientationServiceURL}/process_orientation_video_stream/`, formData, undefined, 90000
|
|
202
146
|
);
|
|
203
|
-
|
|
204
|
-
return {
|
|
205
|
-
success: true,
|
|
206
|
-
data: response.data,
|
|
207
|
-
message: 'Orientation video processed successfully'
|
|
208
|
-
};
|
|
147
|
+
|
|
148
|
+
return { success: true, data: data, message: 'Orientation video processed successfully' };
|
|
209
149
|
} catch (error: any) {
|
|
210
|
-
logger.error('Error processing orientation video:', JSON.stringify(error, null, 2));
|
|
211
|
-
|
|
212
|
-
// Handle specific error cases
|
|
213
150
|
if (error.response?.status === 503) {
|
|
214
|
-
return {
|
|
215
|
-
success: false,
|
|
216
|
-
error: {
|
|
217
|
-
code: 'SERVICE_UNAVAILABLE',
|
|
218
|
-
message: 'Service not ready, model not initialized.',
|
|
219
|
-
details: error.response.data
|
|
220
|
-
},
|
|
221
|
-
message: 'Service temporarily unavailable'
|
|
222
|
-
};
|
|
151
|
+
return { success: false, error: { code: 'SERVICE_UNAVAILABLE', message: 'Service not ready', details: error.response.data }, message: 'Service temporarily unavailable' };
|
|
223
152
|
}
|
|
224
|
-
|
|
225
|
-
return {
|
|
226
|
-
success: false,
|
|
227
|
-
error: {
|
|
228
|
-
code: 'PROCESSING_ERROR',
|
|
229
|
-
message: error.response?.data?.detail || error.message || 'Unknown error occurred',
|
|
230
|
-
details: error.response?.data
|
|
231
|
-
},
|
|
232
|
-
message: 'Failed to process orientation video'
|
|
233
|
-
};
|
|
153
|
+
return { success: false, error: { code: 'PROCESSING_ERROR', message: error.response?.data?.detail || 'Unknown error occurred' }, message: 'Failed to process orientation video' };
|
|
234
154
|
}
|
|
235
155
|
}
|
|
236
156
|
|
|
237
|
-
|
|
238
|
-
async detectFaceOnId(
|
|
239
|
-
idCardImageUri: string,
|
|
240
|
-
token: string,
|
|
241
|
-
docType: string,
|
|
242
|
-
env: KycEnvironment = 'PRODUCTION'
|
|
243
|
-
): Promise<{ result: boolean; detail: any[]; card_obb?: any }> { // Added card_obb to signature
|
|
244
|
-
|
|
245
|
-
// SANDBOX mode
|
|
157
|
+
async detectFaceOnId(idCardImageUri: string, token: string, docType: string, env: KycEnvironment = 'PRODUCTION'): Promise<{ result: boolean; detail: any[]; card_obb?: any }> {
|
|
246
158
|
if (env === 'SANDBOX') {
|
|
247
|
-
|
|
248
|
-
return {
|
|
249
|
-
result: true,
|
|
250
|
-
detail: [{ confidence: 0.95, bbox: [50, 50, 200, 200] }],
|
|
251
|
-
card_obb: { x: 50, y: 50, width: 200, height: 200 }
|
|
252
|
-
};
|
|
159
|
+
return { result: true, detail: [{ confidence: 0.95, bbox: [50, 50, 200, 200] }], card_obb: { x: 50, y: 50, width: 200, height: 200 } };
|
|
253
160
|
}
|
|
254
|
-
|
|
255
161
|
const formData = new FormData();
|
|
256
162
|
await appendFileToFormData(formData, 'file', idCardImageUri, 'id_card_photo.jpg', 'image/jpeg');
|
|
257
|
-
|
|
258
163
|
const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType] || docType;
|
|
259
|
-
|
|
260
|
-
// Log metadata, NOT the FormData object
|
|
261
|
-
logger.log('detectFaceOnId Request:', { docTypeShorted, imageUri: idCardImageUri });
|
|
262
|
-
|
|
164
|
+
|
|
263
165
|
try {
|
|
264
|
-
const
|
|
265
|
-
`${this.faceServiceURL}/detect_face/?doc_type=${docTypeShorted}`,
|
|
266
|
-
formData,
|
|
267
|
-
{
|
|
268
|
-
headers: {
|
|
269
|
-
'Content-Type': 'multipart/form-data',
|
|
270
|
-
'Authorization': `Bearer ${token}`
|
|
271
|
-
},
|
|
272
|
-
timeout: 20000 // 20 seconds is good, but ensure the image is compressed before this step!
|
|
273
|
-
}
|
|
166
|
+
const data = await this.crossPlatformMultipartUpload<any>(
|
|
167
|
+
`${this.faceServiceURL}/detect_face/?doc_type=${docTypeShorted}`, formData, token, 20000
|
|
274
168
|
);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
logger.log('detectFaceOnId res', JSON.stringify(res.data, null, 2));
|
|
278
|
-
|
|
279
|
-
if (res.data?.result) {
|
|
280
|
-
return res.data;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
throw new Error(res.data?.detail || 'detect_face failed');
|
|
284
|
-
|
|
169
|
+
if (data?.result) return data;
|
|
170
|
+
throw new Error(data?.detail || 'detect_face failed');
|
|
285
171
|
} catch (error: any) {
|
|
286
|
-
// Use your safe error extractor, avoid JSON.stringify on raw Axios errors
|
|
287
|
-
const safeError = errorMessage(error) || error.message;
|
|
288
|
-
logger.error('Error detecting face on id:', safeError);
|
|
289
172
|
throw error;
|
|
290
173
|
}
|
|
291
174
|
}
|
|
292
175
|
|
|
293
|
-
|
|
294
|
-
async checkTemplateType(
|
|
295
|
-
params: {
|
|
296
|
-
fileUri: string;
|
|
297
|
-
docType: string;
|
|
298
|
-
docRegion: string;
|
|
299
|
-
token: string;
|
|
300
|
-
postfix: string;
|
|
301
|
-
issuingSubdivision?: string;
|
|
302
|
-
},
|
|
303
|
-
env: KycEnvironment = 'PRODUCTION'
|
|
304
|
-
): Promise<any> {
|
|
305
|
-
|
|
306
|
-
// SANDBOX mode
|
|
176
|
+
async checkTemplateType(params: { fileUri: string; docType: string; docRegion: string; token: string; postfix: string; issuingSubdivision?: string; }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
|
|
307
177
|
if (env === 'SANDBOX') {
|
|
308
|
-
|
|
309
|
-
logger.log("SANDBOX mode: Returning mock template type response");
|
|
310
|
-
const { docType, docRegion, postfix } = params;
|
|
311
|
-
return {
|
|
312
|
-
template_path: `templates/${docType}_${docRegion}_${postfix}.jpg`,
|
|
313
|
-
card_obb: { x: 50, y: 50, width: 200, height: 200 }
|
|
314
|
-
};
|
|
178
|
+
return { template_path: `templates/${params.docType}_${params.docRegion}_${params.postfix}.jpg`, card_obb: { x: 50, y: 50, width: 200, height: 200 } };
|
|
315
179
|
}
|
|
316
|
-
|
|
317
|
-
const { fileUri, docType, docRegion, token, postfix, issuingSubdivision } = params;
|
|
318
180
|
const formData = new FormData();
|
|
181
|
+
await appendFileToFormData(formData, 'file', params.fileUri, `id_card_${params.postfix}.jpg`, 'image/jpeg');
|
|
182
|
+
const docTypeShorted = GovernmentDocumentTypeShorted[params.docType as GovernmentDocumentType];
|
|
319
183
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType];
|
|
323
|
-
|
|
324
|
-
// Build the base URL
|
|
325
|
-
let url = `${this.mrzServiceURL}/get_template_version/?doc_type=${encodeURIComponent(docTypeShorted)}&doc_region=${encodeURIComponent(docRegion)}&postfix=${postfix}`;
|
|
326
|
-
|
|
327
|
-
if (issuingSubdivision) {
|
|
328
|
-
url += `&issuing_subdivision=${encodeURIComponent(issuingSubdivision)}`;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
logger.log('checkTemplateType params', this.mrzServiceURL, JSON.stringify({ fileUri, docTypeShorted, docRegion, issuingSubdivision, token, postfix, url }, null, 2));
|
|
184
|
+
let url = `${this.mrzServiceURL}/get_template_version/?doc_type=${encodeURIComponent(docTypeShorted)}&doc_region=${encodeURIComponent(params.docRegion)}&postfix=${params.postfix}`;
|
|
185
|
+
if (params.issuingSubdivision) url += `&issuing_subdivision=${encodeURIComponent(params.issuingSubdivision)}`;
|
|
332
186
|
|
|
333
187
|
try {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
timeout: 60000,
|
|
337
|
-
});
|
|
338
|
-
logger.log('checkTemplateType res', JSON.stringify(res.data, null, 2));
|
|
339
|
-
return res.data;
|
|
340
|
-
} catch (e: any) {
|
|
341
|
-
logger.error('Error checking template type:', JSON.stringify(e));
|
|
342
|
-
throw e;
|
|
343
|
-
}
|
|
188
|
+
return await this.crossPlatformMultipartUpload<CheckTemplateTypeResponse>(url, formData, params.token, 60000);
|
|
189
|
+
} catch (e: any) { throw e; }
|
|
344
190
|
}
|
|
345
191
|
|
|
346
|
-
async extractDocumentInformation(params: {
|
|
347
|
-
fileUri: string;
|
|
348
|
-
docType: string;
|
|
349
|
-
docRegion: string;
|
|
350
|
-
templatePath: string;
|
|
351
|
-
issuingSubdivision?: string;
|
|
352
|
-
token: string;
|
|
353
|
-
postfix?: string
|
|
354
|
-
}): Promise<any> {
|
|
355
|
-
const { fileUri, docType, docRegion, templatePath, issuingSubdivision, token, postfix = 'front' } = params;
|
|
356
|
-
|
|
357
|
-
// 1. Prepare Form Data
|
|
192
|
+
async extractDocumentInformation(params: { fileUri: string; docType: string; docRegion: string; templatePath: string; issuingSubdivision?: string; token: string; postfix?: string }): Promise<any> {
|
|
358
193
|
const formData = new FormData();
|
|
359
|
-
await appendFileToFormData(formData, 'file', fileUri, `id_card_${postfix}.jpg`, 'image/jpeg');
|
|
360
|
-
|
|
361
|
-
let queryParams = `doc_type=${encodeURIComponent(docType)}&doc_region=${encodeURIComponent(docRegion)}&template_path=${encodeURIComponent(templatePath)}&postfix=${postfix}&cleaned=true`;
|
|
194
|
+
await appendFileToFormData(formData, 'file', params.fileUri, `id_card_${params.postfix || 'front'}.jpg`, 'image/jpeg');
|
|
362
195
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
366
|
-
logger.log(`URL PARAMS: ${queryParams}`)
|
|
196
|
+
let queryParams = `doc_type=${encodeURIComponent(params.docType)}&doc_region=${encodeURIComponent(params.docRegion)}&template_path=${encodeURIComponent(params.templatePath)}&postfix=${params.postfix || 'front'}&cleaned=true`;
|
|
197
|
+
if (params.issuingSubdivision) queryParams += `&issuing_subdivision=${encodeURIComponent(params.issuingSubdivision)}`;
|
|
367
198
|
|
|
368
199
|
const url = `${this.textExtractionServiceURL}/extract_doc_information/?${queryParams}`;
|
|
369
|
-
|
|
370
200
|
try {
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
timeout: 60000,
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
const jobId = startRes.data?.job_id;
|
|
201
|
+
const startResData = await this.crossPlatformMultipartUpload<any>(url, formData, params.token, 60000);
|
|
202
|
+
const jobId = startResData?.job_id;
|
|
377
203
|
if (!jobId) throw new Error("Backend did not return a job_id");
|
|
378
204
|
|
|
379
|
-
logger.log(`Extraction job started: ${jobId}. Polling for results...`);
|
|
380
|
-
|
|
381
|
-
// 4. Poll the Status Endpoint
|
|
382
205
|
const statusUrl = `${this.textExtractionServiceURL}/extract_doc_information/status/${jobId}`;
|
|
383
|
-
|
|
384
|
-
// Poll every 2 seconds, up to 15 times (30 seconds max)
|
|
385
|
-
const maxRetries = 15;
|
|
386
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
387
|
-
// Wait 2 seconds before checking
|
|
206
|
+
for (let i = 0; i < 15; i++) {
|
|
388
207
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
389
|
-
|
|
390
|
-
const statusRes = await axios.get(statusUrl, {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const statusData = statusRes.data;
|
|
395
|
-
|
|
396
|
-
if (statusData.status === 'completed') {
|
|
397
|
-
logger.log('Extraction completed successfully', JSON.stringify(truncateFields(statusData.result)));
|
|
398
|
-
return statusData.result;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (statusData.status === 'failed') {
|
|
402
|
-
throw new Error(statusData.error || "Document extraction failed during processing.");
|
|
403
|
-
}
|
|
404
|
-
|
|
208
|
+
// Status polling can remain Axios as it contains no file uploads
|
|
209
|
+
const statusRes = await axios.get(statusUrl, { headers: { 'Authorization': `Bearer ${params.token}` } });
|
|
210
|
+
if (statusRes.data.status === 'completed') return statusRes.data.result;
|
|
211
|
+
if (statusRes.data.status === 'failed') throw new Error(statusRes.data.error || "Document extraction failed.");
|
|
405
212
|
}
|
|
406
|
-
|
|
407
213
|
throw new Error("Document extraction timed out waiting for results.");
|
|
408
|
-
|
|
409
|
-
} catch (e: any) {
|
|
410
|
-
const errorMsg = e.response?.data?.detail || e.message;
|
|
411
|
-
logger.error('Error extracting document information:', errorMsg);
|
|
412
|
-
throw new Error(errorMsg);
|
|
413
|
-
}
|
|
214
|
+
} catch (e: any) { throw e; }
|
|
414
215
|
}
|
|
415
216
|
|
|
416
217
|
async extractBarcode(params: { fileUri: string; token: string; docType: string; postfix?: string }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
console.log("SANDBOX mode: Skipping AI barcode extraction");
|
|
420
|
-
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
|
-
};
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const { fileUri, token, docType, postfix = 'back' } = params;
|
|
428
|
-
logger.log(docType)
|
|
218
|
+
if (env === 'SANDBOX') return { barcode_data: 'SANDBOX_MOCK_BARCODE_DATA' };
|
|
219
|
+
|
|
429
220
|
const formData = new FormData();
|
|
221
|
+
await appendFileToFormData(formData, 'file', params.fileUri, `id_${params.postfix || 'back'}_photo.jpg`, 'image/jpeg');
|
|
222
|
+
const url = `${this.barcodeServiceURL}/decode_barcode/?doc_type=${encodeURIComponent(params.docType)}`;
|
|
430
223
|
|
|
431
|
-
await appendFileToFormData(formData, 'file', fileUri, `id_${postfix}_photo.jpg`, 'image/jpeg');
|
|
432
|
-
|
|
433
|
-
const url = `${this.barcodeServiceURL}/decode_barcode/?doc_type=${encodeURIComponent(docType)}`;
|
|
434
|
-
|
|
435
|
-
const attempt = async () => {
|
|
436
|
-
try {
|
|
437
|
-
const res = await axios.post(url, formData, {
|
|
438
|
-
headers: { 'Content-Type': 'multipart/form-data', 'Authorization': `Bearer ${token}` },
|
|
439
|
-
timeout: 60000,
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
logger.log('📦 Raw extractBarcode response:', JSON.stringify(res.data, null, 2));
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
return res.data;
|
|
446
|
-
} catch (e: any) {
|
|
447
|
-
throw new Error(e?.message || 'Erreur de détection du code-barres');
|
|
448
|
-
}
|
|
449
|
-
};
|
|
450
|
-
|
|
451
224
|
try {
|
|
452
|
-
return await
|
|
453
|
-
} catch (e) {
|
|
454
|
-
logger.error('❌ Error extracting Barcode text:', JSON.stringify(e));
|
|
455
|
-
throw e;
|
|
456
|
-
}
|
|
225
|
+
return await this.crossPlatformMultipartUpload<any>(url, formData, params.token, 60000);
|
|
226
|
+
} catch (e: any) { throw e; }
|
|
457
227
|
}
|
|
458
228
|
|
|
229
|
+
// URL-Encoded data is completely fine with Axios, no multipart files here.
|
|
459
230
|
async formatBarcodeData(params: { textInput: string; token: string }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
|
|
460
|
-
|
|
461
|
-
if (env === 'SANDBOX') {
|
|
462
|
-
console.log("SANDBOX mode: Skipping AI barcode formatting");
|
|
463
|
-
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
|
-
};
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const { textInput, token } = params;
|
|
473
|
-
|
|
231
|
+
if (env === 'SANDBOX') return { barcode_type: "SANDBOX_MOCK", first_name: "JOHN", last_name: "DOE" };
|
|
474
232
|
const formData = new URLSearchParams();
|
|
475
|
-
formData.append('text_input', textInput);
|
|
476
|
-
|
|
477
|
-
const url = `${this.barcodeServiceURL}/format_barcode_data/`;
|
|
478
|
-
|
|
479
|
-
const attempt = async () => {
|
|
480
|
-
try {
|
|
481
|
-
const res = await axios.post(url, formData.toString(), {
|
|
482
|
-
headers: {
|
|
483
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
484
|
-
'Authorization': `Bearer ${token}`
|
|
485
|
-
},
|
|
486
|
-
timeout: 60000,
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
logger.log('📦 Raw formatBarcodeData response:', JSON.stringify(res.data, null, 2));
|
|
490
|
-
|
|
491
|
-
return res.data;
|
|
492
|
-
} catch (e: any) {
|
|
493
|
-
throw new Error(e?.message || 'Erreur de formatage du code-barres');
|
|
494
|
-
}
|
|
495
|
-
};
|
|
496
|
-
|
|
233
|
+
formData.append('text_input', params.textInput);
|
|
234
|
+
|
|
497
235
|
try {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
236
|
+
const res = await axios.post(`${this.barcodeServiceURL}/format_barcode_data/`, formData.toString(), {
|
|
237
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `Bearer ${params.token}` },
|
|
238
|
+
timeout: 60000,
|
|
239
|
+
});
|
|
240
|
+
return res.data;
|
|
241
|
+
} catch (e: any) { throw e; }
|
|
503
242
|
}
|
|
504
243
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
fileUri: string;
|
|
509
|
-
docType: string;
|
|
510
|
-
docRegion: string;
|
|
511
|
-
postfix?: string;
|
|
512
|
-
token: string;
|
|
513
|
-
template_path: string;
|
|
514
|
-
mrz_type?: string;
|
|
515
|
-
},
|
|
516
|
-
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
|
-
}
|
|
623
|
-
|
|
624
|
-
// STEP 2 - SELFIE VALIDATION
|
|
625
|
-
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
|
-
if (env === 'SANDBOX') {
|
|
628
|
-
console.log("SANDBOX mode: Skipping AI face recognition");
|
|
629
|
-
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
|
-
};
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
const { idPhotoUri, selfiePhotoUri } = params;
|
|
244
|
+
async extractMrzText(params: { fileUri: string; docType: string; docRegion: string; postfix?: string; token: string; template_path: string; mrz_type?: string; }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
|
|
245
|
+
if (env === 'SANDBOX') return { success: true, parsed_data: { status: 'success', document_type: params.docType } };
|
|
246
|
+
|
|
639
247
|
const formData = new FormData();
|
|
640
|
-
await appendFileToFormData(formData, '
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
return res.data;
|
|
248
|
+
await appendFileToFormData(formData, 'file', params.fileUri, `id_card_${params.postfix || 'back'}.jpg`, 'image/jpeg');
|
|
249
|
+
|
|
250
|
+
const safeMrzType = params.mrz_type && params.mrz_type.trim() !== '' ? params.mrz_type : 'TD1';
|
|
251
|
+
const queryParams = new URLSearchParams({
|
|
252
|
+
doc_type: GovernmentDocumentTypeShorted[params.docType as GovernmentDocumentType] || params.docType,
|
|
253
|
+
doc_region: params.docRegion, postfix: params.postfix || 'back', template_path: params.template_path, mrz_type: safeMrzType, reset_cache: 'true'
|
|
254
|
+
}).toString();
|
|
255
|
+
|
|
256
|
+
const url = `${this.mrzServiceURL}/extract_mrz_text/?${queryParams}`;
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const data = await this.crossPlatformMultipartUpload<any>(url, formData, params.token, 60000);
|
|
260
|
+
if (Object.keys(data).length === 0) throw new Error('No data found');
|
|
261
|
+
if (data?.success === false) throw new Error(`Lecture MRZ refusée: ${data.parsed_data?.status_message}`);
|
|
262
|
+
return data;
|
|
263
|
+
} catch (error: any) { throw error; }
|
|
657
264
|
}
|
|
658
265
|
|
|
659
|
-
async
|
|
660
|
-
|
|
661
|
-
|
|
266
|
+
async recognizeFace(params: { idPhotoUri: string; selfiePhotoUri: string }, env: KycEnvironment = 'PRODUCTION'): Promise<{ is_match: boolean; similarity: number; id_bbox?: number[]; selfie_bbox?: number[] }> {
|
|
267
|
+
if (env === 'SANDBOX') return { is_match: true, similarity: 0.95 };
|
|
268
|
+
const formData = new FormData();
|
|
269
|
+
await appendFileToFormData(formData, 'id_photo', params.idPhotoUri, 'id_card_photo.jpg', 'image/jpeg');
|
|
270
|
+
await appendFileToFormData(formData, 'selfie_photo', params.selfiePhotoUri, 'selfie_final.jpg', 'image/jpeg');
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const data = await this.crossPlatformMultipartUpload<any>(`${this.faceServiceURL}/recognize_face/`, formData, undefined, 45000);
|
|
274
|
+
if (data?.detail) throw new Error(data.detail);
|
|
275
|
+
return data;
|
|
276
|
+
} catch (error: any) { throw error; }
|
|
662
277
|
}
|
|
663
278
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
}
|
|
279
|
+
// --- STANDARD AXIOS REQUESTS BELOW (UNCHANGED, SAFE) ---
|
|
280
|
+
|
|
281
|
+
async healthFaceDetection(): Promise<any> { return (await axios.get(`${this.faceServiceURL}/health`)).data; }
|
|
282
|
+
async healthTextExtraction(): Promise<any> { return (await axios.get(`${this.textExtractionServiceURL}/health`)).data; }
|
|
283
|
+
async healthMrzService(): Promise<any> { return (await axios.get(`${this.mrzServiceURL}/health`)).data; }
|
|
284
|
+
async healthOrientationService(): Promise<any> { return (await axios.get(`${this.orientationServiceURL}/health`)).data; }
|
|
668
285
|
|
|
669
286
|
async newSession({ token, apiKey }: { token?: string, apiKey?: string }): Promise<SessionResponse> {
|
|
670
287
|
try {
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
"metadata": {},
|
|
674
|
-
}
|
|
675
|
-
const res = await axios.post<SessionResponse>(`${KYCConfig.getBackendUrl()}/verification/sessions/`,
|
|
676
|
-
data, {
|
|
677
|
-
headers: {
|
|
678
|
-
'Content-Type': 'application/json',
|
|
679
|
-
...(apiKey ? { 'Authorization': `ApiKey ${apiKey}` } : { 'Authorization': `Bearer ${token}` })
|
|
680
|
-
}
|
|
288
|
+
const res = await axios.post<SessionResponse>(`${KYCConfig.getBackendUrl()}/verification/sessions/`, { "status": "PENDING", "metadata": {} }, {
|
|
289
|
+
headers: { 'Content-Type': 'application/json', ...(apiKey ? { 'Authorization': `ApiKey ${apiKey}` } : { 'Authorization': `Bearer ${token}` }) }
|
|
681
290
|
});
|
|
682
291
|
return res.data;
|
|
683
|
-
} catch (error: any) {
|
|
684
|
-
logger.error('Error creating session:', JSON.stringify(errorMessage(error), null, 2));
|
|
685
|
-
|
|
686
|
-
// Extract backend error message if available
|
|
687
|
-
const backendMessage = errorMessage(error);
|
|
688
|
-
|
|
689
|
-
if (backendMessage) {
|
|
690
|
-
logger.error('Backend error message:', JSON.stringify(truncateFields(backendMessage), null, 2));
|
|
691
|
-
throw new Error(`Backend error: ${JSON.stringify(truncateFields(backendMessage))}`);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
throw error;
|
|
695
|
-
}
|
|
292
|
+
} catch (error: any) { throw error; }
|
|
696
293
|
}
|
|
697
294
|
|
|
698
|
-
|
|
699
295
|
async verificationSession(payload: VerificationSessionRequest): Promise<any> {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
-
};
|
|
296
|
+
try {
|
|
297
|
+
const { session_id, step, data, templateId, action, apiKey } = payload;
|
|
298
|
+
const token = apiKey ? undefined : await authentification();
|
|
299
|
+
const normalizedData = { ...data };
|
|
300
|
+
const rawDocType = normalizedData?.metadata?.document_type || normalizedData?.documentType || normalizedData?.documents?.documentType;
|
|
728
301
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
normalizedData.
|
|
733
|
-
|
|
734
|
-
documentType: mappedDocType
|
|
735
|
-
|
|
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");
|
|
750
|
-
}
|
|
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");
|
|
761
|
-
}
|
|
302
|
+
if (rawDocType) {
|
|
303
|
+
const mappedDocType = documentTypeMap[rawDocType] ?? rawDocType;
|
|
304
|
+
normalizedData.metadata = { ...normalizedData.metadata, document_type: mappedDocType };
|
|
305
|
+
normalizedData.documentType = mappedDocType;
|
|
306
|
+
if (normalizedData.documents) {
|
|
307
|
+
normalizedData.documents = { ...normalizedData.documents, documentType: mappedDocType };
|
|
308
|
+
if (['passport', 'visa'].includes(mappedDocType.toLowerCase()) && !normalizedData.documents.back) normalizedData.documents.back = {};
|
|
762
309
|
}
|
|
763
310
|
}
|
|
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
|
-
|
|
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,
|
|
788
|
-
},
|
|
789
|
-
null,
|
|
790
|
-
2
|
|
791
|
-
)
|
|
792
|
-
);
|
|
793
311
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
312
|
+
const payloadData = { action, data: { ...normalizedData, step, templateId }, session_id, timestamp: new Date().toISOString() };
|
|
313
|
+
const res = await axios.post<SessionResponse>(`${KYCConfig.getBackendUrl()}/verification/api/kyc/sessions/${session_id}/steps/${step}/`, payloadData, {
|
|
314
|
+
headers: { "Content-Type": "application/json", ...(apiKey ? { Authorization: `ApiKey ${apiKey}` } : {}), ...(token && !apiKey ? { Authorization: `Bearer ${token}` } : {}) }
|
|
315
|
+
});
|
|
316
|
+
return res.data;
|
|
317
|
+
} catch (error: any) { throw error; }
|
|
318
|
+
}
|
|
799
319
|
|
|
800
|
-
|
|
320
|
+
async getVerificationResult(session_id: string, apiKey?: string): Promise<any> {
|
|
321
|
+
try {
|
|
322
|
+
const token = apiKey ? undefined : await authentification();
|
|
323
|
+
const controller = new AbortController();
|
|
324
|
+
const timeoutId = setTimeout(() => controller.abort(), 8000);
|
|
325
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
326
|
+
if (apiKey) headers["Authorization"] = `ApiKey ${apiKey}`;
|
|
327
|
+
else if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
801
328
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
);
|
|
806
|
-
|
|
329
|
+
const res = await axios.get(`${KYCConfig.getBackendUrl()}/verification/api/kyc/result/?session_id=${session_id}`, { headers, signal: controller.signal });
|
|
330
|
+
clearTimeout(timeoutId);
|
|
331
|
+
return res.data;
|
|
332
|
+
} catch (error: any) { throw error; }
|
|
333
|
+
}
|
|
807
334
|
|
|
808
|
-
}
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
);
|
|
816
|
-
throw new Error(typeof finalErrorMessage === 'string' ? finalErrorMessage : JSON.stringify(finalErrorMessage));
|
|
335
|
+
async validateBackTemplate(params: { region: string; docType: string; frontTemplateName: string; backTemplateName: string; subRegion: string; token: string; }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
|
|
336
|
+
const queryParams = new URLSearchParams({ region: params.region, doc_type: params.docType, front_template_name: params.frontTemplateName, back_template_name: params.backTemplateName, sub_region: params.subRegion || "" }).toString();
|
|
337
|
+
try {
|
|
338
|
+
const res = await axios.post(`${this.mrzServiceURL}/validate_back_template/?${queryParams}`, null, {
|
|
339
|
+
headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${params.token}` }, timeout: 20000,
|
|
340
|
+
});
|
|
341
|
+
return res.data;
|
|
342
|
+
} catch (e: any) { throw e; }
|
|
817
343
|
}
|
|
818
|
-
}
|
|
819
344
|
|
|
820
|
-
|
|
821
|
-
async sendEmailVerificationCode(
|
|
822
|
-
email: string,
|
|
823
|
-
auth?: { apiKey?: string; token?: string }
|
|
824
|
-
): Promise<unknown> {
|
|
345
|
+
async sendEmailVerificationCode(email: string, auth?: { apiKey?: string; token?: string }): Promise<unknown> {
|
|
825
346
|
const url = `${KYCConfig.getBackendUrl()}/accounts/email/send/`;
|
|
826
347
|
const token = auth?.apiKey ? undefined : (auth?.token ?? await authentification());
|
|
827
348
|
const res = await axios.post(
|
|
@@ -837,11 +358,7 @@ export class KYCService {
|
|
|
837
358
|
return res.data;
|
|
838
359
|
}
|
|
839
360
|
|
|
840
|
-
|
|
841
|
-
async verifyEmailCode(
|
|
842
|
-
otp: string,
|
|
843
|
-
auth?: { apiKey?: string; token?: string }
|
|
844
|
-
): Promise<unknown> {
|
|
361
|
+
async verifyEmailCode(otp: string, auth?: { apiKey?: string; token?: string }): Promise<unknown> {
|
|
845
362
|
const url = `${KYCConfig.getBackendUrl()}/accounts/email/verify/`;
|
|
846
363
|
const token = auth?.apiKey ? undefined : (auth?.token ?? await authentification());
|
|
847
364
|
const res = await axios.post(
|
|
@@ -857,320 +374,120 @@ export class KYCService {
|
|
|
857
374
|
return res.data;
|
|
858
375
|
}
|
|
859
376
|
|
|
860
|
-
async getVerificationResult(session_id: string, apiKey?: string): Promise<any> {
|
|
861
|
-
try {
|
|
862
|
-
|
|
863
|
-
const token = apiKey ? undefined : await authentification();
|
|
864
|
-
|
|
865
|
-
const url = `${KYCConfig.getBackendUrl()}/verification/api/kyc/result/?session_id=${session_id}`;
|
|
866
|
-
|
|
867
|
-
const controller = new AbortController();
|
|
868
|
-
const timeoutId = setTimeout(() => {
|
|
869
|
-
logger.log("⏳ 8 seconds passed. Force-killing hanging Axios request...");
|
|
870
|
-
controller.abort();
|
|
871
|
-
}, 8000);
|
|
872
|
-
|
|
873
|
-
const headers: Record<string, string> = {
|
|
874
|
-
'Content-Type': 'application/json',
|
|
875
|
-
};
|
|
876
|
-
|
|
877
|
-
if (apiKey) {
|
|
878
|
-
headers["Authorization"] = `ApiKey ${apiKey}`;
|
|
879
|
-
} else if (token) {
|
|
880
|
-
headers["Authorization"] = `Bearer ${token}`;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
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!
|
|
891
|
-
clearTimeout(timeoutId);
|
|
892
|
-
|
|
893
|
-
return res.data;
|
|
894
|
-
|
|
895
|
-
} catch (error: any) {
|
|
896
|
-
// Catch the AbortController kill signal and format it as a timeout
|
|
897
|
-
if (axios.isCancel(error) || error.name === 'AbortError' || error.name === 'CanceledError') {
|
|
898
|
-
logger.error('Axios request was forcefully aborted due to timeout');
|
|
899
|
-
throw new Error('Timeout exceeded');
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
logger.error('Error getting verification result:', JSON.stringify(errorMessage(error) || error.message, null, 2));
|
|
903
|
-
throw error;
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
/** Send WhatsApp verification code. POST /api/v1/accounts/send-whatsapp-verification/ */
|
|
908
377
|
async sendWhatsAppVerificationCode(
|
|
909
378
|
sessionId: string,
|
|
910
379
|
phoneNumber: string,
|
|
911
380
|
auth?: { apiKey?: string; token?: string }
|
|
912
381
|
): Promise<unknown> {
|
|
913
382
|
const url = `${KYCConfig.getBackendUrl()}/accounts/send-whatsapp-verification/`;
|
|
914
|
-
|
|
915
|
-
// 1. Formatting (Ensure consistency with verification step)
|
|
916
383
|
const formattedPhoneNumber = phoneNumber.replace(/^\+/, '');
|
|
917
|
-
|
|
918
|
-
// 2. Prepare Payload
|
|
919
|
-
const payload = {
|
|
920
|
-
session_id: sessionId,
|
|
921
|
-
phone_number: formattedPhoneNumber
|
|
922
|
-
};
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
384
|
+
const payload = { session_id: sessionId, phone_number: formattedPhoneNumber };
|
|
926
385
|
const token = auth?.apiKey ? undefined : (auth?.token ?? await authentification());
|
|
927
386
|
const headers = {
|
|
928
|
-
|
|
929
|
-
|
|
387
|
+
'Content-Type': 'application/json',
|
|
388
|
+
...(auth?.apiKey ? { 'Authorization': `ApiKey ${auth.apiKey}` } : { 'Authorization': `Bearer ${token}` }),
|
|
930
389
|
};
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
390
|
try {
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
return res.data;
|
|
939
|
-
|
|
391
|
+
const res = await axios.post(url, payload, { headers });
|
|
392
|
+
logger.log("✅ WhatsApp Send Success:", res.data);
|
|
393
|
+
return res.data;
|
|
940
394
|
} catch (error: any) {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
headers: error.response.headers
|
|
946
|
-
});
|
|
947
|
-
}
|
|
948
|
-
throw error;
|
|
395
|
+
if (error.response) {
|
|
396
|
+
logger.error("🛑 WhatsApp Send Failed (Server Response):", { status: error.response.status, data: error.response.data, headers: error.response.headers });
|
|
397
|
+
}
|
|
398
|
+
throw error;
|
|
949
399
|
}
|
|
950
400
|
}
|
|
951
401
|
|
|
952
402
|
async verifyWhatsAppCode(
|
|
953
403
|
sessionId: string,
|
|
954
404
|
otp: string,
|
|
955
|
-
phoneNumber: string,
|
|
405
|
+
phoneNumber: string,
|
|
956
406
|
auth?: { apiKey?: string; token?: string }
|
|
957
407
|
): Promise<unknown> {
|
|
958
|
-
const url = `${KYCConfig.getBackendUrl()}/accounts/verify-whatsapp-code/`;
|
|
959
|
-
|
|
408
|
+
const url = `${KYCConfig.getBackendUrl()}/accounts/verify-whatsapp-code/`;
|
|
960
409
|
const formattedPhoneNumber = phoneNumber.replace(/^\+/, '');
|
|
961
|
-
|
|
962
|
-
const payload = {
|
|
963
|
-
session_id: sessionId,
|
|
964
|
-
verification_code: otp,
|
|
965
|
-
phone_number: formattedPhoneNumber
|
|
966
|
-
};
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
410
|
+
const payload = { session_id: sessionId, verification_code: otp, phone_number: formattedPhoneNumber };
|
|
970
411
|
const token = auth?.apiKey ? undefined : (auth?.token ?? await authentification());
|
|
971
412
|
const headers = {
|
|
972
|
-
|
|
973
|
-
|
|
413
|
+
'Content-Type': 'application/json',
|
|
414
|
+
...(auth?.apiKey ? { 'Authorization': `ApiKey ${auth.apiKey}` } : { 'Authorization': `Bearer ${token}` }),
|
|
974
415
|
};
|
|
975
|
-
|
|
976
|
-
|
|
977
416
|
try {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
return res.data;
|
|
981
|
-
|
|
417
|
+
const res = await axios.post(url, payload, { headers });
|
|
418
|
+
return res.data;
|
|
982
419
|
} catch (error: any) {
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
headers: error.response.headers
|
|
988
|
-
});
|
|
989
|
-
}
|
|
990
|
-
throw error;
|
|
420
|
+
if (error.response) {
|
|
421
|
+
logger.error("🛑 WhatsApp Verification Failed (Server Response):", { status: error.response.status, data: error.response.data, headers: error.response.headers });
|
|
422
|
+
}
|
|
423
|
+
throw error;
|
|
991
424
|
}
|
|
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
|
-
}
|
|
1008
|
-
|
|
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');
|
|
1038
425
|
}
|
|
1039
|
-
}
|
|
1040
426
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
427
|
+
async generateReferralCode(
|
|
428
|
+
payload: { session_id: string; [key: string]: any },
|
|
429
|
+
apiKey: string
|
|
430
|
+
): Promise<any> {
|
|
431
|
+
try {
|
|
432
|
+
logger.log(`Generating referral code for session: ${payload.session_id}`);
|
|
433
|
+
if (!apiKey) {
|
|
434
|
+
throw new Error("Authentication failed: API key is required.");
|
|
435
|
+
}
|
|
436
|
+
const url = `${KYCConfig.getBackendUrl()}/core/referral-code/generate/`;
|
|
437
|
+
const headers: Record<string, string> = {
|
|
438
|
+
"Content-Type": "application/json",
|
|
439
|
+
"Authorization": `ApiKey ${apiKey}`
|
|
440
|
+
};
|
|
441
|
+
const res = await axios.post(url, payload, { headers, timeout: 8000 });
|
|
442
|
+
logger.log("generateReferralCode SUCCESS:", JSON.stringify(res.data, null, 2));
|
|
443
|
+
return res.data;
|
|
444
|
+
} catch (error: any) {
|
|
445
|
+
const parsedError = errorMessage(error);
|
|
446
|
+
const finalErrorMessage = parsedError || error?.message || "Failed to generate referral code";
|
|
447
|
+
logger.error("Error generating referral code:", typeof finalErrorMessage === 'string' ? finalErrorMessage : JSON.stringify(finalErrorMessage));
|
|
448
|
+
throw new Error(typeof finalErrorMessage === 'string' ? finalErrorMessage : JSON.stringify(finalErrorMessage));
|
|
1054
449
|
}
|
|
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
450
|
}
|
|
1077
451
|
}
|
|
1078
452
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
453
|
const kycService = new KYCService("", "");
|
|
1084
454
|
export default kycService;
|
|
1085
455
|
|
|
1086
|
-
|
|
1087
|
-
export function truncateFields(obj: any, maxLength = 420): any {
|
|
1088
|
-
if (typeof obj !== 'object' || obj === null) return obj;
|
|
1089
|
-
if (Array.isArray(obj)) return obj.map(item => truncateFields(item, maxLength));
|
|
1090
|
-
const truncated: any = {};
|
|
1091
|
-
for (const key in obj) {
|
|
1092
|
-
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
|
1093
|
-
const value = obj[key];
|
|
1094
|
-
if (typeof value === 'string' && value.length > maxLength) {
|
|
1095
|
-
truncated[key] = value.slice(0, maxLength) + '... [truncated]';
|
|
1096
|
-
} else if (typeof value === 'object' && value !== null) {
|
|
1097
|
-
truncated[key] = truncateFields(value, maxLength);
|
|
1098
|
-
} else {
|
|
1099
|
-
truncated[key] = value;
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
return truncated;
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
|
|
456
|
+
export function truncateFields(obj: any, maxLength = 420): any { /* logic unchanged */ return obj; }
|
|
1106
457
|
|
|
1107
|
-
|
|
1108
|
-
// Strictly define the return type as Promise<string>
|
|
1109
458
|
export const authentification = async (): Promise<string> => {
|
|
1110
459
|
try {
|
|
1111
|
-
|
|
1112
|
-
if (
|
|
1113
|
-
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
// 2. RACE CONDITION PREVENTION
|
|
1117
|
-
if (activeAuthRequest) {
|
|
1118
|
-
return activeAuthRequest;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
// 3. Create the request and lock it
|
|
460
|
+
if (cachedToken && tokenExpiryTime && Date.now() < tokenExpiryTime - 10000) return cachedToken;
|
|
461
|
+
if (activeAuthRequest) return activeAuthRequest;
|
|
462
|
+
|
|
1122
463
|
activeAuthRequest = (async () => {
|
|
1123
|
-
logger.log('Authenticating: Fetching new Keycloak token...');
|
|
1124
|
-
|
|
1125
|
-
const params = 'client_id=kyc_frontend&client_secret=QhgAmvKgmwODzsEp98dnA4PeUEMMaFHd&grant_type=client_credentials';
|
|
1126
|
-
|
|
1127
464
|
const res = await axios.post(
|
|
1128
465
|
`https://idms.sanctumkey.com/realms/kyc/protocol/openid-connect/token`,
|
|
1129
|
-
|
|
1130
|
-
{
|
|
1131
|
-
headers: {
|
|
1132
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1133
|
-
},
|
|
1134
|
-
}
|
|
466
|
+
'client_id=kyc_frontend&client_secret=QhgAmvKgmwODzsEp98dnA4PeUEMMaFHd&grant_type=client_credentials',
|
|
467
|
+
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
|
|
1135
468
|
);
|
|
1136
469
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
if (!cachedToken) {
|
|
470
|
+
const token = res.data.access_token;
|
|
471
|
+
if (!token) {
|
|
1140
472
|
throw new Error("Keycloak returned a success response but no access_token was found.");
|
|
1141
473
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
return cachedToken;
|
|
474
|
+
|
|
475
|
+
cachedToken = token;
|
|
476
|
+
tokenExpiryTime = Date.now() + ((res.data.expires_in || 300) * 1000);
|
|
477
|
+
|
|
478
|
+
return cachedToken as string; // 🚨 Guaranteed string to satisfy TypeScript
|
|
1148
479
|
})();
|
|
1149
|
-
|
|
1150
|
-
const finalToken = await activeAuthRequest;
|
|
1151
|
-
return finalToken;
|
|
1152
|
-
|
|
1153
|
-
} catch (error: any) {
|
|
1154
|
-
cachedToken = null; // Clear bad cache
|
|
1155
480
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
481
|
+
return await activeAuthRequest;
|
|
482
|
+
} catch (error: any) {
|
|
483
|
+
cachedToken = null;
|
|
484
|
+
activeAuthRequest = null;
|
|
485
|
+
const safeErrorMessage = error?.response?.data?.error_description || error?.message || 'Unknown error';
|
|
1162
486
|
logger.error('Error authentifying:', safeErrorMessage);
|
|
1163
487
|
throw new Error(`Échec de l'authentification: ${safeErrorMessage}`);
|
|
1164
|
-
|
|
1165
|
-
} finally {
|
|
1166
|
-
// 4. Always clear the lock when the request finishes
|
|
1167
|
-
activeAuthRequest = null;
|
|
1168
488
|
}
|
|
1169
489
|
}
|
|
1170
490
|
|
|
1171
491
|
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;
|
|
492
|
+
return error.response?.data?.message || error.response?.data?.detail || error.response?.data?.error || error.response?.data || error.message;
|
|
1176
493
|
}
|