@koraidv/core 1.5.0
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/dist/index.d.mts +1049 -0
- package/dist/index.d.ts +1049 -0
- package/dist/index.js +1938 -0
- package/dist/index.mjs +1893 -0
- package/package.json +44 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1893 @@
|
|
|
1
|
+
// src/types/DocumentType.ts
|
|
2
|
+
var DocumentType = /* @__PURE__ */ ((DocumentType2) => {
|
|
3
|
+
DocumentType2["US_DRIVERS_LICENSE"] = "us_drivers_license";
|
|
4
|
+
DocumentType2["US_STATE_ID"] = "us_state_id";
|
|
5
|
+
DocumentType2["US_GREEN_CARD"] = "us_green_card";
|
|
6
|
+
DocumentType2["INTERNATIONAL_PASSPORT"] = "international_passport";
|
|
7
|
+
DocumentType2["EU_ID_GERMANY"] = "eu_id_de";
|
|
8
|
+
DocumentType2["EU_ID_FRANCE"] = "eu_id_fr";
|
|
9
|
+
DocumentType2["EU_ID_SPAIN"] = "eu_id_es";
|
|
10
|
+
DocumentType2["EU_ID_ITALY"] = "eu_id_it";
|
|
11
|
+
DocumentType2["GHANA_CARD"] = "ghana_card";
|
|
12
|
+
DocumentType2["NIGERIA_NIN"] = "ng_nin";
|
|
13
|
+
DocumentType2["NIGERIA_DRIVERS_LICENSE"] = "ng_drivers_license";
|
|
14
|
+
DocumentType2["GHANA_DRIVERS_LICENSE"] = "gh_drivers_license";
|
|
15
|
+
DocumentType2["KENYA_ID"] = "ke_id";
|
|
16
|
+
DocumentType2["KENYA_DRIVERS_LICENSE"] = "ke_drivers_license";
|
|
17
|
+
DocumentType2["SOUTH_AFRICA_ID"] = "za_id";
|
|
18
|
+
DocumentType2["SOUTH_AFRICA_DRIVERS_LICENSE"] = "za_drivers_license";
|
|
19
|
+
DocumentType2["UK_DRIVERS_LICENSE"] = "uk_drivers_license";
|
|
20
|
+
DocumentType2["NIGERIA_VOTERS_CARD"] = "ng_voters_card";
|
|
21
|
+
DocumentType2["LIBERIA_ID"] = "lr_id";
|
|
22
|
+
DocumentType2["LIBERIA_DRIVERS_LICENSE"] = "lr_drivers_license";
|
|
23
|
+
DocumentType2["LIBERIA_VOTERS_CARD"] = "lr_voters_card";
|
|
24
|
+
DocumentType2["SIERRA_LEONE_ID"] = "sl_id";
|
|
25
|
+
DocumentType2["SIERRA_LEONE_DRIVERS_LICENSE"] = "sl_drivers_license";
|
|
26
|
+
DocumentType2["SIERRA_LEONE_VOTERS_CARD"] = "sl_voters_card";
|
|
27
|
+
DocumentType2["GAMBIA_ID"] = "gm_id";
|
|
28
|
+
DocumentType2["GAMBIA_DRIVERS_LICENSE"] = "gm_drivers_license";
|
|
29
|
+
DocumentType2["UK_BRP"] = "uk_brp";
|
|
30
|
+
DocumentType2["CANADA_DRIVERS_LICENSE"] = "ca_drivers_license";
|
|
31
|
+
DocumentType2["CANADA_PR_CARD"] = "ca_pr_card";
|
|
32
|
+
DocumentType2["CANADA_NATIONAL_ID"] = "ca_national_id";
|
|
33
|
+
DocumentType2["INDIA_DRIVERS_LICENSE"] = "in_drivers_license";
|
|
34
|
+
DocumentType2["GERMANY_RP"] = "de_rp";
|
|
35
|
+
DocumentType2["FRANCE_RP"] = "fr_rp";
|
|
36
|
+
DocumentType2["ITALY_RP"] = "it_rp";
|
|
37
|
+
DocumentType2["SPAIN_RP"] = "es_rp";
|
|
38
|
+
DocumentType2["IRELAND_RP"] = "ie_rp";
|
|
39
|
+
DocumentType2["PORTUGAL_RP"] = "pt_rp";
|
|
40
|
+
DocumentType2["SWEDEN_RP"] = "se_rp";
|
|
41
|
+
DocumentType2["DENMARK_RP"] = "dk_rp";
|
|
42
|
+
DocumentType2["NORWAY_RP"] = "no_rp";
|
|
43
|
+
DocumentType2["FINLAND_RP"] = "fi_rp";
|
|
44
|
+
DocumentType2["POLAND_RP"] = "pl_rp";
|
|
45
|
+
return DocumentType2;
|
|
46
|
+
})(DocumentType || {});
|
|
47
|
+
function getDocumentTypeInfo(type) {
|
|
48
|
+
const info = {
|
|
49
|
+
["us_drivers_license" /* US_DRIVERS_LICENSE */]: {
|
|
50
|
+
code: "us_drivers_license" /* US_DRIVERS_LICENSE */,
|
|
51
|
+
displayName: "US Driver's License",
|
|
52
|
+
hasMRZ: false,
|
|
53
|
+
requiresBack: true
|
|
54
|
+
},
|
|
55
|
+
["us_state_id" /* US_STATE_ID */]: {
|
|
56
|
+
code: "us_state_id" /* US_STATE_ID */,
|
|
57
|
+
displayName: "US State ID",
|
|
58
|
+
hasMRZ: false,
|
|
59
|
+
requiresBack: true
|
|
60
|
+
},
|
|
61
|
+
["us_green_card" /* US_GREEN_CARD */]: {
|
|
62
|
+
code: "us_green_card" /* US_GREEN_CARD */,
|
|
63
|
+
displayName: "US Permanent Resident Card",
|
|
64
|
+
hasMRZ: true,
|
|
65
|
+
requiresBack: true
|
|
66
|
+
},
|
|
67
|
+
["international_passport" /* INTERNATIONAL_PASSPORT */]: {
|
|
68
|
+
code: "international_passport" /* INTERNATIONAL_PASSPORT */,
|
|
69
|
+
displayName: "International Passport",
|
|
70
|
+
hasMRZ: true,
|
|
71
|
+
requiresBack: false
|
|
72
|
+
},
|
|
73
|
+
["eu_id_de" /* EU_ID_GERMANY */]: {
|
|
74
|
+
code: "eu_id_de" /* EU_ID_GERMANY */,
|
|
75
|
+
displayName: "German ID Card",
|
|
76
|
+
hasMRZ: true,
|
|
77
|
+
requiresBack: true
|
|
78
|
+
},
|
|
79
|
+
["eu_id_fr" /* EU_ID_FRANCE */]: {
|
|
80
|
+
code: "eu_id_fr" /* EU_ID_FRANCE */,
|
|
81
|
+
displayName: "French ID Card",
|
|
82
|
+
hasMRZ: true,
|
|
83
|
+
requiresBack: true
|
|
84
|
+
},
|
|
85
|
+
["eu_id_es" /* EU_ID_SPAIN */]: {
|
|
86
|
+
code: "eu_id_es" /* EU_ID_SPAIN */,
|
|
87
|
+
displayName: "Spanish ID Card",
|
|
88
|
+
hasMRZ: true,
|
|
89
|
+
requiresBack: true
|
|
90
|
+
},
|
|
91
|
+
["eu_id_it" /* EU_ID_ITALY */]: {
|
|
92
|
+
code: "eu_id_it" /* EU_ID_ITALY */,
|
|
93
|
+
displayName: "Italian ID Card",
|
|
94
|
+
hasMRZ: true,
|
|
95
|
+
requiresBack: true
|
|
96
|
+
},
|
|
97
|
+
["ghana_card" /* GHANA_CARD */]: {
|
|
98
|
+
code: "ghana_card" /* GHANA_CARD */,
|
|
99
|
+
displayName: "Ghana Card",
|
|
100
|
+
hasMRZ: false,
|
|
101
|
+
requiresBack: false
|
|
102
|
+
},
|
|
103
|
+
["ng_nin" /* NIGERIA_NIN */]: {
|
|
104
|
+
code: "ng_nin" /* NIGERIA_NIN */,
|
|
105
|
+
displayName: "Nigeria NIN",
|
|
106
|
+
hasMRZ: false,
|
|
107
|
+
requiresBack: false
|
|
108
|
+
},
|
|
109
|
+
["ke_id" /* KENYA_ID */]: {
|
|
110
|
+
code: "ke_id" /* KENYA_ID */,
|
|
111
|
+
displayName: "Kenya ID",
|
|
112
|
+
hasMRZ: false,
|
|
113
|
+
requiresBack: true
|
|
114
|
+
},
|
|
115
|
+
["za_id" /* SOUTH_AFRICA_ID */]: {
|
|
116
|
+
code: "za_id" /* SOUTH_AFRICA_ID */,
|
|
117
|
+
displayName: "South Africa ID",
|
|
118
|
+
hasMRZ: false,
|
|
119
|
+
requiresBack: false
|
|
120
|
+
},
|
|
121
|
+
["ng_drivers_license" /* NIGERIA_DRIVERS_LICENSE */]: {
|
|
122
|
+
code: "ng_drivers_license" /* NIGERIA_DRIVERS_LICENSE */,
|
|
123
|
+
displayName: "Driver's License",
|
|
124
|
+
hasMRZ: false,
|
|
125
|
+
requiresBack: true
|
|
126
|
+
},
|
|
127
|
+
["gh_drivers_license" /* GHANA_DRIVERS_LICENSE */]: {
|
|
128
|
+
code: "gh_drivers_license" /* GHANA_DRIVERS_LICENSE */,
|
|
129
|
+
displayName: "Driver's License",
|
|
130
|
+
hasMRZ: false,
|
|
131
|
+
requiresBack: true
|
|
132
|
+
},
|
|
133
|
+
["ke_drivers_license" /* KENYA_DRIVERS_LICENSE */]: {
|
|
134
|
+
code: "ke_drivers_license" /* KENYA_DRIVERS_LICENSE */,
|
|
135
|
+
displayName: "Driver's License",
|
|
136
|
+
hasMRZ: false,
|
|
137
|
+
requiresBack: true
|
|
138
|
+
},
|
|
139
|
+
["za_drivers_license" /* SOUTH_AFRICA_DRIVERS_LICENSE */]: {
|
|
140
|
+
code: "za_drivers_license" /* SOUTH_AFRICA_DRIVERS_LICENSE */,
|
|
141
|
+
displayName: "Driver's License",
|
|
142
|
+
hasMRZ: false,
|
|
143
|
+
requiresBack: true
|
|
144
|
+
},
|
|
145
|
+
["uk_drivers_license" /* UK_DRIVERS_LICENSE */]: {
|
|
146
|
+
code: "uk_drivers_license" /* UK_DRIVERS_LICENSE */,
|
|
147
|
+
displayName: "Driver's License",
|
|
148
|
+
hasMRZ: false,
|
|
149
|
+
requiresBack: true
|
|
150
|
+
},
|
|
151
|
+
["ca_drivers_license" /* CANADA_DRIVERS_LICENSE */]: {
|
|
152
|
+
code: "ca_drivers_license" /* CANADA_DRIVERS_LICENSE */,
|
|
153
|
+
displayName: "Driver's License",
|
|
154
|
+
hasMRZ: false,
|
|
155
|
+
requiresBack: true
|
|
156
|
+
},
|
|
157
|
+
["ca_pr_card" /* CANADA_PR_CARD */]: {
|
|
158
|
+
code: "ca_pr_card" /* CANADA_PR_CARD */,
|
|
159
|
+
displayName: "Canadian Permanent Resident Card",
|
|
160
|
+
hasMRZ: true,
|
|
161
|
+
requiresBack: true
|
|
162
|
+
},
|
|
163
|
+
["ca_national_id" /* CANADA_NATIONAL_ID */]: {
|
|
164
|
+
code: "ca_national_id" /* CANADA_NATIONAL_ID */,
|
|
165
|
+
displayName: "Canadian National Identity Card",
|
|
166
|
+
hasMRZ: true,
|
|
167
|
+
requiresBack: false
|
|
168
|
+
},
|
|
169
|
+
["in_drivers_license" /* INDIA_DRIVERS_LICENSE */]: {
|
|
170
|
+
code: "in_drivers_license" /* INDIA_DRIVERS_LICENSE */,
|
|
171
|
+
displayName: "Driver's License",
|
|
172
|
+
hasMRZ: false,
|
|
173
|
+
requiresBack: true
|
|
174
|
+
},
|
|
175
|
+
["ng_voters_card" /* NIGERIA_VOTERS_CARD */]: {
|
|
176
|
+
code: "ng_voters_card" /* NIGERIA_VOTERS_CARD */,
|
|
177
|
+
displayName: "Voter's Card",
|
|
178
|
+
hasMRZ: false,
|
|
179
|
+
requiresBack: true
|
|
180
|
+
},
|
|
181
|
+
["lr_id" /* LIBERIA_ID */]: {
|
|
182
|
+
code: "lr_id" /* LIBERIA_ID */,
|
|
183
|
+
displayName: "Liberia ID",
|
|
184
|
+
hasMRZ: false,
|
|
185
|
+
requiresBack: true
|
|
186
|
+
},
|
|
187
|
+
["lr_drivers_license" /* LIBERIA_DRIVERS_LICENSE */]: {
|
|
188
|
+
code: "lr_drivers_license" /* LIBERIA_DRIVERS_LICENSE */,
|
|
189
|
+
displayName: "Driver's License",
|
|
190
|
+
hasMRZ: false,
|
|
191
|
+
requiresBack: true
|
|
192
|
+
},
|
|
193
|
+
["lr_voters_card" /* LIBERIA_VOTERS_CARD */]: {
|
|
194
|
+
code: "lr_voters_card" /* LIBERIA_VOTERS_CARD */,
|
|
195
|
+
displayName: "Voter's Card",
|
|
196
|
+
hasMRZ: false,
|
|
197
|
+
requiresBack: true
|
|
198
|
+
},
|
|
199
|
+
["sl_id" /* SIERRA_LEONE_ID */]: {
|
|
200
|
+
code: "sl_id" /* SIERRA_LEONE_ID */,
|
|
201
|
+
displayName: "Sierra Leone ID",
|
|
202
|
+
hasMRZ: false,
|
|
203
|
+
requiresBack: true
|
|
204
|
+
},
|
|
205
|
+
["sl_drivers_license" /* SIERRA_LEONE_DRIVERS_LICENSE */]: {
|
|
206
|
+
code: "sl_drivers_license" /* SIERRA_LEONE_DRIVERS_LICENSE */,
|
|
207
|
+
displayName: "Driver's License",
|
|
208
|
+
hasMRZ: false,
|
|
209
|
+
requiresBack: true
|
|
210
|
+
},
|
|
211
|
+
["sl_voters_card" /* SIERRA_LEONE_VOTERS_CARD */]: {
|
|
212
|
+
code: "sl_voters_card" /* SIERRA_LEONE_VOTERS_CARD */,
|
|
213
|
+
displayName: "Voter's Card",
|
|
214
|
+
hasMRZ: false,
|
|
215
|
+
requiresBack: true
|
|
216
|
+
},
|
|
217
|
+
["gm_id" /* GAMBIA_ID */]: {
|
|
218
|
+
code: "gm_id" /* GAMBIA_ID */,
|
|
219
|
+
displayName: "Gambia ID",
|
|
220
|
+
hasMRZ: false,
|
|
221
|
+
requiresBack: true
|
|
222
|
+
},
|
|
223
|
+
["gm_drivers_license" /* GAMBIA_DRIVERS_LICENSE */]: {
|
|
224
|
+
code: "gm_drivers_license" /* GAMBIA_DRIVERS_LICENSE */,
|
|
225
|
+
displayName: "Driver's License",
|
|
226
|
+
hasMRZ: false,
|
|
227
|
+
requiresBack: true
|
|
228
|
+
},
|
|
229
|
+
["uk_brp" /* UK_BRP */]: {
|
|
230
|
+
code: "uk_brp" /* UK_BRP */,
|
|
231
|
+
displayName: "UK Biometric Residence Permit",
|
|
232
|
+
hasMRZ: true,
|
|
233
|
+
requiresBack: true
|
|
234
|
+
},
|
|
235
|
+
["de_rp" /* GERMANY_RP */]: {
|
|
236
|
+
code: "de_rp" /* GERMANY_RP */,
|
|
237
|
+
displayName: "Germany Residence Permit",
|
|
238
|
+
hasMRZ: true,
|
|
239
|
+
requiresBack: true
|
|
240
|
+
},
|
|
241
|
+
["fr_rp" /* FRANCE_RP */]: {
|
|
242
|
+
code: "fr_rp" /* FRANCE_RP */,
|
|
243
|
+
displayName: "France Residence Permit",
|
|
244
|
+
hasMRZ: true,
|
|
245
|
+
requiresBack: true
|
|
246
|
+
},
|
|
247
|
+
["it_rp" /* ITALY_RP */]: {
|
|
248
|
+
code: "it_rp" /* ITALY_RP */,
|
|
249
|
+
displayName: "Italy Residence Permit",
|
|
250
|
+
hasMRZ: true,
|
|
251
|
+
requiresBack: true
|
|
252
|
+
},
|
|
253
|
+
["es_rp" /* SPAIN_RP */]: {
|
|
254
|
+
code: "es_rp" /* SPAIN_RP */,
|
|
255
|
+
displayName: "Spain Residence Permit",
|
|
256
|
+
hasMRZ: true,
|
|
257
|
+
requiresBack: true
|
|
258
|
+
},
|
|
259
|
+
["ie_rp" /* IRELAND_RP */]: {
|
|
260
|
+
code: "ie_rp" /* IRELAND_RP */,
|
|
261
|
+
displayName: "Ireland Residence Permit",
|
|
262
|
+
hasMRZ: true,
|
|
263
|
+
requiresBack: true
|
|
264
|
+
},
|
|
265
|
+
["pt_rp" /* PORTUGAL_RP */]: {
|
|
266
|
+
code: "pt_rp" /* PORTUGAL_RP */,
|
|
267
|
+
displayName: "Portugal Residence Permit",
|
|
268
|
+
hasMRZ: true,
|
|
269
|
+
requiresBack: true
|
|
270
|
+
},
|
|
271
|
+
["se_rp" /* SWEDEN_RP */]: {
|
|
272
|
+
code: "se_rp" /* SWEDEN_RP */,
|
|
273
|
+
displayName: "Sweden Residence Permit",
|
|
274
|
+
hasMRZ: true,
|
|
275
|
+
requiresBack: true
|
|
276
|
+
},
|
|
277
|
+
["dk_rp" /* DENMARK_RP */]: {
|
|
278
|
+
code: "dk_rp" /* DENMARK_RP */,
|
|
279
|
+
displayName: "Denmark Residence Permit",
|
|
280
|
+
hasMRZ: true,
|
|
281
|
+
requiresBack: true
|
|
282
|
+
},
|
|
283
|
+
["no_rp" /* NORWAY_RP */]: {
|
|
284
|
+
code: "no_rp" /* NORWAY_RP */,
|
|
285
|
+
displayName: "Norway Residence Permit",
|
|
286
|
+
hasMRZ: true,
|
|
287
|
+
requiresBack: true
|
|
288
|
+
},
|
|
289
|
+
["fi_rp" /* FINLAND_RP */]: {
|
|
290
|
+
code: "fi_rp" /* FINLAND_RP */,
|
|
291
|
+
displayName: "Finland Residence Permit",
|
|
292
|
+
hasMRZ: true,
|
|
293
|
+
requiresBack: true
|
|
294
|
+
},
|
|
295
|
+
["pl_rp" /* POLAND_RP */]: {
|
|
296
|
+
code: "pl_rp" /* POLAND_RP */,
|
|
297
|
+
displayName: "Poland Residence Permit",
|
|
298
|
+
hasMRZ: true,
|
|
299
|
+
requiresBack: true
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
return info[type];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/types/Configuration.ts
|
|
306
|
+
var environmentUrls = {
|
|
307
|
+
production: "https://api.korastratum.com/api/v1/idv",
|
|
308
|
+
sandbox: "https://koraidv-identity-sandbox-626704085312.us-central1.run.app/api/v1"
|
|
309
|
+
};
|
|
310
|
+
var defaultTheme = {
|
|
311
|
+
primaryColor: "#2563EB",
|
|
312
|
+
backgroundColor: "#FFFFFF",
|
|
313
|
+
surfaceColor: "#F8FAFC",
|
|
314
|
+
textColor: "#1E293B",
|
|
315
|
+
secondaryTextColor: "#64748B",
|
|
316
|
+
errorColor: "#DC2626",
|
|
317
|
+
successColor: "#16A34A",
|
|
318
|
+
borderRadius: 12
|
|
319
|
+
};
|
|
320
|
+
var defaultConfiguration = {
|
|
321
|
+
environment: "production",
|
|
322
|
+
documentTypes: Object.values(DocumentType),
|
|
323
|
+
livenessMode: "active",
|
|
324
|
+
theme: defaultTheme,
|
|
325
|
+
locale: { language: "en" },
|
|
326
|
+
timeout: 600,
|
|
327
|
+
debugLogging: false
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// src/types/KoraError.ts
|
|
331
|
+
var KoraErrorCode = /* @__PURE__ */ ((KoraErrorCode2) => {
|
|
332
|
+
KoraErrorCode2["NOT_CONFIGURED"] = "NOT_CONFIGURED";
|
|
333
|
+
KoraErrorCode2["INVALID_API_KEY"] = "INVALID_API_KEY";
|
|
334
|
+
KoraErrorCode2["INVALID_TENANT_ID"] = "INVALID_TENANT_ID";
|
|
335
|
+
KoraErrorCode2["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
336
|
+
KoraErrorCode2["TIMEOUT"] = "TIMEOUT";
|
|
337
|
+
KoraErrorCode2["NO_INTERNET"] = "NO_INTERNET";
|
|
338
|
+
KoraErrorCode2["UNAUTHORIZED"] = "UNAUTHORIZED";
|
|
339
|
+
KoraErrorCode2["FORBIDDEN"] = "FORBIDDEN";
|
|
340
|
+
KoraErrorCode2["NOT_FOUND"] = "NOT_FOUND";
|
|
341
|
+
KoraErrorCode2["VALIDATION_ERROR"] = "VALIDATION_ERROR";
|
|
342
|
+
KoraErrorCode2["RATE_LIMITED"] = "RATE_LIMITED";
|
|
343
|
+
KoraErrorCode2["SERVER_ERROR"] = "SERVER_ERROR";
|
|
344
|
+
KoraErrorCode2["HTTP_ERROR"] = "HTTP_ERROR";
|
|
345
|
+
KoraErrorCode2["CAMERA_ACCESS_DENIED"] = "CAMERA_ACCESS_DENIED";
|
|
346
|
+
KoraErrorCode2["CAMERA_NOT_AVAILABLE"] = "CAMERA_NOT_AVAILABLE";
|
|
347
|
+
KoraErrorCode2["CAPTURE_FAILED"] = "CAPTURE_FAILED";
|
|
348
|
+
KoraErrorCode2["QUALITY_VALIDATION_FAILED"] = "QUALITY_VALIDATION_FAILED";
|
|
349
|
+
KoraErrorCode2["DOCUMENT_NOT_DETECTED"] = "DOCUMENT_NOT_DETECTED";
|
|
350
|
+
KoraErrorCode2["DOCUMENT_TYPE_NOT_SUPPORTED"] = "DOCUMENT_TYPE_NOT_SUPPORTED";
|
|
351
|
+
KoraErrorCode2["MRZ_READ_FAILED"] = "MRZ_READ_FAILED";
|
|
352
|
+
KoraErrorCode2["FACE_NOT_DETECTED"] = "FACE_NOT_DETECTED";
|
|
353
|
+
KoraErrorCode2["MULTIPLE_FACES_DETECTED"] = "MULTIPLE_FACES_DETECTED";
|
|
354
|
+
KoraErrorCode2["FACE_MATCH_FAILED"] = "FACE_MATCH_FAILED";
|
|
355
|
+
KoraErrorCode2["LIVENESS_CHECK_FAILED"] = "LIVENESS_CHECK_FAILED";
|
|
356
|
+
KoraErrorCode2["CHALLENGE_NOT_COMPLETED"] = "CHALLENGE_NOT_COMPLETED";
|
|
357
|
+
KoraErrorCode2["SESSION_EXPIRED"] = "SESSION_EXPIRED";
|
|
358
|
+
KoraErrorCode2["VERIFICATION_EXPIRED"] = "VERIFICATION_EXPIRED";
|
|
359
|
+
KoraErrorCode2["VERIFICATION_ALREADY_COMPLETED"] = "VERIFICATION_ALREADY_COMPLETED";
|
|
360
|
+
KoraErrorCode2["INVALID_VERIFICATION_STATE"] = "INVALID_VERIFICATION_STATE";
|
|
361
|
+
KoraErrorCode2["UNKNOWN"] = "UNKNOWN";
|
|
362
|
+
KoraErrorCode2["USER_CANCELLED"] = "USER_CANCELLED";
|
|
363
|
+
return KoraErrorCode2;
|
|
364
|
+
})(KoraErrorCode || {});
|
|
365
|
+
var errorMessages = {
|
|
366
|
+
["NOT_CONFIGURED" /* NOT_CONFIGURED */]: "SDK not configured. Initialize KoraIDV first.",
|
|
367
|
+
["INVALID_API_KEY" /* INVALID_API_KEY */]: "Invalid API key provided.",
|
|
368
|
+
["INVALID_TENANT_ID" /* INVALID_TENANT_ID */]: "Invalid tenant ID provided.",
|
|
369
|
+
["NETWORK_ERROR" /* NETWORK_ERROR */]: "Network error. Please check your connection.",
|
|
370
|
+
["TIMEOUT" /* TIMEOUT */]: "Request timed out. Please try again.",
|
|
371
|
+
["NO_INTERNET" /* NO_INTERNET */]: "No internet connection.",
|
|
372
|
+
["UNAUTHORIZED" /* UNAUTHORIZED */]: "Authentication failed. Check your API key.",
|
|
373
|
+
["FORBIDDEN" /* FORBIDDEN */]: "Access denied.",
|
|
374
|
+
["NOT_FOUND" /* NOT_FOUND */]: "Resource not found.",
|
|
375
|
+
["VALIDATION_ERROR" /* VALIDATION_ERROR */]: "Validation error.",
|
|
376
|
+
["RATE_LIMITED" /* RATE_LIMITED */]: "Rate limit exceeded. Please try again later.",
|
|
377
|
+
["SERVER_ERROR" /* SERVER_ERROR */]: "Server error. Please try again later.",
|
|
378
|
+
["HTTP_ERROR" /* HTTP_ERROR */]: "HTTP error occurred.",
|
|
379
|
+
["CAMERA_ACCESS_DENIED" /* CAMERA_ACCESS_DENIED */]: "Camera access denied. Please enable camera access.",
|
|
380
|
+
["CAMERA_NOT_AVAILABLE" /* CAMERA_NOT_AVAILABLE */]: "Camera not available on this device.",
|
|
381
|
+
["CAPTURE_FAILED" /* CAPTURE_FAILED */]: "Capture failed.",
|
|
382
|
+
["QUALITY_VALIDATION_FAILED" /* QUALITY_VALIDATION_FAILED */]: "Quality check failed.",
|
|
383
|
+
["DOCUMENT_NOT_DETECTED" /* DOCUMENT_NOT_DETECTED */]: "Document not detected. Position document in frame.",
|
|
384
|
+
["DOCUMENT_TYPE_NOT_SUPPORTED" /* DOCUMENT_TYPE_NOT_SUPPORTED */]: "Document type not supported.",
|
|
385
|
+
["MRZ_READ_FAILED" /* MRZ_READ_FAILED */]: "Could not read document MRZ.",
|
|
386
|
+
["FACE_NOT_DETECTED" /* FACE_NOT_DETECTED */]: "Face not detected. Position face in frame.",
|
|
387
|
+
["MULTIPLE_FACES_DETECTED" /* MULTIPLE_FACES_DETECTED */]: "Multiple faces detected. Show only one face.",
|
|
388
|
+
["FACE_MATCH_FAILED" /* FACE_MATCH_FAILED */]: "Face match failed.",
|
|
389
|
+
["LIVENESS_CHECK_FAILED" /* LIVENESS_CHECK_FAILED */]: "Liveness check failed.",
|
|
390
|
+
["CHALLENGE_NOT_COMPLETED" /* CHALLENGE_NOT_COMPLETED */]: "Challenge not completed.",
|
|
391
|
+
["SESSION_EXPIRED" /* SESSION_EXPIRED */]: "Session expired. Please start over.",
|
|
392
|
+
["VERIFICATION_EXPIRED" /* VERIFICATION_EXPIRED */]: "Verification expired. Please start a new one.",
|
|
393
|
+
["VERIFICATION_ALREADY_COMPLETED" /* VERIFICATION_ALREADY_COMPLETED */]: "Verification already completed.",
|
|
394
|
+
["INVALID_VERIFICATION_STATE" /* INVALID_VERIFICATION_STATE */]: "Invalid verification state.",
|
|
395
|
+
["UNKNOWN" /* UNKNOWN */]: "An unknown error occurred.",
|
|
396
|
+
["USER_CANCELLED" /* USER_CANCELLED */]: "Verification cancelled."
|
|
397
|
+
};
|
|
398
|
+
var recoverySuggestions = {
|
|
399
|
+
["CAMERA_ACCESS_DENIED" /* CAMERA_ACCESS_DENIED */]: "Go to browser settings and enable camera access.",
|
|
400
|
+
["NO_INTERNET" /* NO_INTERNET */]: "Check your Wi-Fi or cellular connection.",
|
|
401
|
+
["TIMEOUT" /* TIMEOUT */]: "Please wait a moment and try again.",
|
|
402
|
+
["RATE_LIMITED" /* RATE_LIMITED */]: "Please wait a moment and try again.",
|
|
403
|
+
["SERVER_ERROR" /* SERVER_ERROR */]: "Please wait a moment and try again.",
|
|
404
|
+
["DOCUMENT_NOT_DETECTED" /* DOCUMENT_NOT_DETECTED */]: "Place document on flat surface with good lighting.",
|
|
405
|
+
["FACE_NOT_DETECTED" /* FACE_NOT_DETECTED */]: "Ensure good lighting and center your face.",
|
|
406
|
+
["QUALITY_VALIDATION_FAILED" /* QUALITY_VALIDATION_FAILED */]: "Hold device steady and ensure good lighting."
|
|
407
|
+
};
|
|
408
|
+
var KoraError = class _KoraError extends Error {
|
|
409
|
+
constructor(code, details) {
|
|
410
|
+
const message = errorMessages[code] || "An error occurred";
|
|
411
|
+
super(message);
|
|
412
|
+
this.name = "KoraError";
|
|
413
|
+
this.code = code;
|
|
414
|
+
this.recoverySuggestion = recoverySuggestions[code];
|
|
415
|
+
this.details = details;
|
|
416
|
+
if (Error.captureStackTrace) {
|
|
417
|
+
Error.captureStackTrace(this, _KoraError);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Create error from HTTP status code
|
|
422
|
+
*/
|
|
423
|
+
static fromHttpStatus(status, details) {
|
|
424
|
+
const codeMap = {
|
|
425
|
+
401: "UNAUTHORIZED" /* UNAUTHORIZED */,
|
|
426
|
+
403: "FORBIDDEN" /* FORBIDDEN */,
|
|
427
|
+
404: "NOT_FOUND" /* NOT_FOUND */,
|
|
428
|
+
422: "VALIDATION_ERROR" /* VALIDATION_ERROR */,
|
|
429
|
+
429: "RATE_LIMITED" /* RATE_LIMITED */
|
|
430
|
+
};
|
|
431
|
+
if (status >= 500) {
|
|
432
|
+
return new _KoraError("SERVER_ERROR" /* SERVER_ERROR */, details);
|
|
433
|
+
}
|
|
434
|
+
return new _KoraError(codeMap[status] || "HTTP_ERROR" /* HTTP_ERROR */, details);
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Check if error is retryable
|
|
438
|
+
*/
|
|
439
|
+
get isRetryable() {
|
|
440
|
+
return [
|
|
441
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
442
|
+
"TIMEOUT" /* TIMEOUT */,
|
|
443
|
+
"RATE_LIMITED" /* RATE_LIMITED */,
|
|
444
|
+
"SERVER_ERROR" /* SERVER_ERROR */
|
|
445
|
+
].includes(this.code);
|
|
446
|
+
}
|
|
447
|
+
toJSON() {
|
|
448
|
+
return {
|
|
449
|
+
name: this.name,
|
|
450
|
+
code: this.code,
|
|
451
|
+
message: this.message,
|
|
452
|
+
recoverySuggestion: this.recoverySuggestion,
|
|
453
|
+
details: this.details
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// src/utils/blob.ts
|
|
459
|
+
function blobToBase64(blob) {
|
|
460
|
+
return new Promise((resolve, reject) => {
|
|
461
|
+
const reader = new FileReader();
|
|
462
|
+
reader.onload = () => {
|
|
463
|
+
const result = reader.result;
|
|
464
|
+
if (typeof result !== "string") {
|
|
465
|
+
reject(new Error("FileReader returned non-string result"));
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const comma = result.indexOf(",");
|
|
469
|
+
resolve(comma >= 0 ? result.slice(comma + 1) : result);
|
|
470
|
+
};
|
|
471
|
+
reader.onerror = () => reject(reader.error ?? new Error("FileReader error"));
|
|
472
|
+
reader.readAsDataURL(blob);
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/api/ApiClient.ts
|
|
477
|
+
var ApiClient = class {
|
|
478
|
+
constructor(configuration) {
|
|
479
|
+
this.maxRetries = 3;
|
|
480
|
+
this.baseDelay = 1e3;
|
|
481
|
+
this.configuration = configuration;
|
|
482
|
+
this.baseUrl = environmentUrls[configuration.environment];
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Get supported countries and their document types
|
|
486
|
+
*/
|
|
487
|
+
async getSupportedCountries() {
|
|
488
|
+
return this.request("/supported-countries");
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Create a new verification
|
|
492
|
+
*/
|
|
493
|
+
async createVerification(request) {
|
|
494
|
+
return this.request("/verifications", {
|
|
495
|
+
method: "POST",
|
|
496
|
+
body: JSON.stringify({
|
|
497
|
+
external_id: request.externalId,
|
|
498
|
+
tier: request.tier
|
|
499
|
+
})
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Get an existing verification
|
|
504
|
+
*/
|
|
505
|
+
async getVerification(id) {
|
|
506
|
+
return this.request(`/verifications/${id}`);
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Upload document image.
|
|
510
|
+
*
|
|
511
|
+
* `decodedBarcodePayload` is the optional Phase 4 fast-path: when the
|
|
512
|
+
* client decoded the PDF417 / QR / DataMatrix on-device using the
|
|
513
|
+
* browser's BarcodeDetector API (or a polyfill), the AAMVA payload
|
|
514
|
+
* travels here so the server can skip image-based barcode decoding
|
|
515
|
+
* (~1-3 s round-trip savings). Empty/`undefined` = server falls
|
|
516
|
+
* back to its zxing-cpp + pdf417decoder cascade. Only meaningful for
|
|
517
|
+
* back captures on documents that carry a barcode.
|
|
518
|
+
* See `docs/architecture/idv-decode-roadmap.md` Phase 4.
|
|
519
|
+
*/
|
|
520
|
+
async uploadDocument(verificationId, imageData, side, documentType, decodedBarcodePayload) {
|
|
521
|
+
if (side === "back") {
|
|
522
|
+
const imageBase64 = await blobToBase64(imageData);
|
|
523
|
+
return this.request(
|
|
524
|
+
`/verifications/${verificationId}/document/back`,
|
|
525
|
+
{
|
|
526
|
+
method: "POST",
|
|
527
|
+
headers: { "Content-Type": "application/json" },
|
|
528
|
+
body: JSON.stringify({
|
|
529
|
+
imageBase64,
|
|
530
|
+
decodedBarcodePayload: decodedBarcodePayload ?? null
|
|
531
|
+
})
|
|
532
|
+
}
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
const formData = new FormData();
|
|
536
|
+
formData.append("image", imageData, "document.jpg");
|
|
537
|
+
formData.append("document_type", documentType);
|
|
538
|
+
formData.append("side", side);
|
|
539
|
+
return this.request(
|
|
540
|
+
`/verifications/${verificationId}/document`,
|
|
541
|
+
{
|
|
542
|
+
method: "POST",
|
|
543
|
+
body: formData,
|
|
544
|
+
headers: {}
|
|
545
|
+
// Let browser set Content-Type for FormData
|
|
546
|
+
}
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Upload selfie image
|
|
551
|
+
*/
|
|
552
|
+
async uploadSelfie(verificationId, imageData) {
|
|
553
|
+
const formData = new FormData();
|
|
554
|
+
formData.append("image", imageData, "selfie.jpg");
|
|
555
|
+
return this.request(`/verifications/${verificationId}/selfie`, {
|
|
556
|
+
method: "POST",
|
|
557
|
+
body: formData,
|
|
558
|
+
headers: {}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Create liveness session
|
|
563
|
+
*/
|
|
564
|
+
async createLivenessSession(verificationId) {
|
|
565
|
+
const response = await this.request(`/verifications/${verificationId}/liveness/session`, {
|
|
566
|
+
method: "POST"
|
|
567
|
+
});
|
|
568
|
+
return {
|
|
569
|
+
sessionId: response.session_id,
|
|
570
|
+
challenges: response.challenges.map((c) => ({
|
|
571
|
+
id: c.id,
|
|
572
|
+
type: c.type,
|
|
573
|
+
instruction: c.instruction,
|
|
574
|
+
order: c.order
|
|
575
|
+
})),
|
|
576
|
+
expiresAt: new Date(response.expires_at)
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Submit liveness challenge
|
|
581
|
+
*/
|
|
582
|
+
async submitLivenessChallenge(verificationId, challenge, imageData) {
|
|
583
|
+
const formData = new FormData();
|
|
584
|
+
formData.append("image", imageData, "challenge.jpg");
|
|
585
|
+
formData.append("challenge_type", challenge.type);
|
|
586
|
+
formData.append("challenge_id", challenge.id);
|
|
587
|
+
const response = await this.request(`/verifications/${verificationId}/liveness/challenge`, {
|
|
588
|
+
method: "POST",
|
|
589
|
+
body: formData,
|
|
590
|
+
headers: {}
|
|
591
|
+
});
|
|
592
|
+
return {
|
|
593
|
+
success: response.success,
|
|
594
|
+
challengePassed: response.challenge_passed,
|
|
595
|
+
confidence: response.confidence,
|
|
596
|
+
remainingChallenges: response.remaining_challenges
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Check document quality before uploading
|
|
601
|
+
*/
|
|
602
|
+
async checkDocumentQuality(imageData, documentType) {
|
|
603
|
+
const base64 = await this.blobToBase64(imageData);
|
|
604
|
+
return this.request("/kyc/document-quality", {
|
|
605
|
+
method: "POST",
|
|
606
|
+
body: JSON.stringify({
|
|
607
|
+
document_front_base64: base64,
|
|
608
|
+
document_type: documentType
|
|
609
|
+
})
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Complete the verification
|
|
614
|
+
*/
|
|
615
|
+
async completeVerification(verificationId) {
|
|
616
|
+
return this.request(`/verifications/${verificationId}/complete`, {
|
|
617
|
+
method: "POST"
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
// ─── QR Handoff (REQ-006) ────────────────────────────────────────────────
|
|
621
|
+
/**
|
|
622
|
+
* Create a handoff session for cross-device mobile capture.
|
|
623
|
+
* Returns a token and capture URL to encode in a QR code.
|
|
624
|
+
*/
|
|
625
|
+
async createHandoffSession(verificationId) {
|
|
626
|
+
return this.request(`/verifications/${verificationId}/handoff-session`, {
|
|
627
|
+
method: "POST"
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Validate a handoff token (called by the mobile capture page).
|
|
632
|
+
* Returns the verification context needed to continue capture.
|
|
633
|
+
*/
|
|
634
|
+
async validateHandoffToken(token) {
|
|
635
|
+
return this.request(`/handoff/${token}`);
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Subscribe to verification status events via Server-Sent Events.
|
|
639
|
+
* Returns an EventSource that emits 'status' and 'complete' events.
|
|
640
|
+
*/
|
|
641
|
+
subscribeToVerificationEvents(verificationId) {
|
|
642
|
+
const url = `${this.baseUrl}/verifications/${verificationId}/events`;
|
|
643
|
+
const eventSource = new EventSource(url, { withCredentials: false });
|
|
644
|
+
return eventSource;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Make an API request with retry logic
|
|
648
|
+
*/
|
|
649
|
+
async request(endpoint, options = {}) {
|
|
650
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
651
|
+
const headers = new Headers(options.headers);
|
|
652
|
+
if (!headers.has("Authorization")) {
|
|
653
|
+
headers.set("Authorization", this.configuration.apiKey);
|
|
654
|
+
}
|
|
655
|
+
headers.set("X-Tenant-ID", this.configuration.tenantId);
|
|
656
|
+
headers.set("Accept", "application/json");
|
|
657
|
+
if (!(options.body instanceof FormData) && !headers.has("Content-Type")) {
|
|
658
|
+
headers.set("Content-Type", "application/json");
|
|
659
|
+
}
|
|
660
|
+
const requestOptions = {
|
|
661
|
+
...options,
|
|
662
|
+
headers
|
|
663
|
+
};
|
|
664
|
+
return this.executeWithRetry(url, requestOptions, 0);
|
|
665
|
+
}
|
|
666
|
+
async executeWithRetry(url, options, attempt) {
|
|
667
|
+
try {
|
|
668
|
+
if (this.configuration.debugLogging) {
|
|
669
|
+
console.log(`[KoraIDV] Request: ${options.method || "GET"} ${url}`);
|
|
670
|
+
}
|
|
671
|
+
const response = await fetch(url, options);
|
|
672
|
+
if (this.configuration.debugLogging) {
|
|
673
|
+
console.log(`[KoraIDV] Response: ${response.status}`);
|
|
674
|
+
}
|
|
675
|
+
if (response.ok) {
|
|
676
|
+
const data = await response.json();
|
|
677
|
+
return this.transformResponse(data);
|
|
678
|
+
}
|
|
679
|
+
if (this.shouldRetry(response.status, attempt)) {
|
|
680
|
+
const delay = this.calculateDelay(attempt, response);
|
|
681
|
+
if (this.configuration.debugLogging) {
|
|
682
|
+
console.log(`[KoraIDV] Retrying in ${delay}ms (attempt ${attempt + 1}/${this.maxRetries})`);
|
|
683
|
+
}
|
|
684
|
+
await this.sleep(delay);
|
|
685
|
+
return this.executeWithRetry(url, options, attempt + 1);
|
|
686
|
+
}
|
|
687
|
+
const errorData = await response.json().catch(() => ({}));
|
|
688
|
+
throw KoraError.fromHttpStatus(response.status, errorData);
|
|
689
|
+
} catch (error) {
|
|
690
|
+
if (error instanceof KoraError) {
|
|
691
|
+
throw error;
|
|
692
|
+
}
|
|
693
|
+
if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
694
|
+
if (this.shouldRetryNetworkError(attempt)) {
|
|
695
|
+
const delay = this.calculateDelay(attempt);
|
|
696
|
+
await this.sleep(delay);
|
|
697
|
+
return this.executeWithRetry(url, options, attempt + 1);
|
|
698
|
+
}
|
|
699
|
+
throw new KoraError("NETWORK_ERROR" /* NETWORK_ERROR */, error.message);
|
|
700
|
+
}
|
|
701
|
+
throw new KoraError("UNKNOWN" /* UNKNOWN */, String(error));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
shouldRetry(status, attempt) {
|
|
705
|
+
if (attempt >= this.maxRetries) return false;
|
|
706
|
+
return status === 429 || status >= 500 && status < 600;
|
|
707
|
+
}
|
|
708
|
+
shouldRetryNetworkError(attempt) {
|
|
709
|
+
return attempt < this.maxRetries;
|
|
710
|
+
}
|
|
711
|
+
calculateDelay(attempt, response) {
|
|
712
|
+
if (response) {
|
|
713
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
714
|
+
if (retryAfter) {
|
|
715
|
+
const seconds = parseInt(retryAfter, 10);
|
|
716
|
+
if (!isNaN(seconds)) {
|
|
717
|
+
return seconds * 1e3;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
const exponentialDelay = this.baseDelay * Math.pow(2, attempt);
|
|
722
|
+
const jitter = Math.random() * 500;
|
|
723
|
+
return exponentialDelay + jitter;
|
|
724
|
+
}
|
|
725
|
+
sleep(ms) {
|
|
726
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
727
|
+
}
|
|
728
|
+
blobToBase64(blob) {
|
|
729
|
+
return new Promise((resolve, reject) => {
|
|
730
|
+
const reader = new FileReader();
|
|
731
|
+
reader.onloadend = () => {
|
|
732
|
+
const result = reader.result;
|
|
733
|
+
const base64 = result.split(",")[1] || result;
|
|
734
|
+
resolve(base64);
|
|
735
|
+
};
|
|
736
|
+
reader.onerror = reject;
|
|
737
|
+
reader.readAsDataURL(blob);
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Transform snake_case response to camelCase
|
|
742
|
+
*/
|
|
743
|
+
transformResponse(data) {
|
|
744
|
+
if (Array.isArray(data)) {
|
|
745
|
+
return data.map((item) => this.transformResponse(item));
|
|
746
|
+
}
|
|
747
|
+
if (data !== null && typeof data === "object") {
|
|
748
|
+
const transformed = {};
|
|
749
|
+
for (const [key, value] of Object.entries(data)) {
|
|
750
|
+
const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
751
|
+
transformed[camelKey] = this.transformResponse(value);
|
|
752
|
+
}
|
|
753
|
+
return transformed;
|
|
754
|
+
}
|
|
755
|
+
return data;
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
// src/KoraIDV.ts
|
|
760
|
+
var KoraIDV = class {
|
|
761
|
+
constructor(config) {
|
|
762
|
+
this.currentVerification = null;
|
|
763
|
+
this.livenessSession = null;
|
|
764
|
+
this.sessionStartTime = null;
|
|
765
|
+
this.configuration = {
|
|
766
|
+
...defaultConfiguration,
|
|
767
|
+
...config,
|
|
768
|
+
environment: config.environment ?? this.detectEnvironment(config.apiKey)
|
|
769
|
+
};
|
|
770
|
+
this.apiClient = new ApiClient(this.configuration);
|
|
771
|
+
}
|
|
772
|
+
detectEnvironment(apiKey) {
|
|
773
|
+
return apiKey.startsWith("ck_sandbox_") ? "sandbox" : "production";
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Get supported countries and their document types from the API
|
|
777
|
+
*/
|
|
778
|
+
async getSupportedCountries() {
|
|
779
|
+
return this.apiClient.getSupportedCountries();
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Start a new verification flow
|
|
783
|
+
*/
|
|
784
|
+
async startVerification(options, callbacks) {
|
|
785
|
+
try {
|
|
786
|
+
this.sessionStartTime = /* @__PURE__ */ new Date();
|
|
787
|
+
const verification = await this.apiClient.createVerification({
|
|
788
|
+
externalId: options.externalId,
|
|
789
|
+
tier: options.tier ?? "standard"
|
|
790
|
+
});
|
|
791
|
+
this.currentVerification = verification;
|
|
792
|
+
callbacks.onStepChange?.("consent");
|
|
793
|
+
} catch (error) {
|
|
794
|
+
callbacks.onError?.(error instanceof KoraError ? error : new KoraError("UNKNOWN" /* UNKNOWN */, String(error)));
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Resume an existing verification
|
|
799
|
+
*/
|
|
800
|
+
async resumeVerification(verificationId, callbacks) {
|
|
801
|
+
try {
|
|
802
|
+
this.sessionStartTime = /* @__PURE__ */ new Date();
|
|
803
|
+
const verification = await this.apiClient.getVerification(verificationId);
|
|
804
|
+
this.currentVerification = verification;
|
|
805
|
+
const step = this.determineStepFromStatus(verification.status);
|
|
806
|
+
callbacks.onStepChange?.(step);
|
|
807
|
+
} catch (error) {
|
|
808
|
+
callbacks.onError?.(error instanceof KoraError ? error : new KoraError("UNKNOWN" /* UNKNOWN */, String(error)));
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Check document quality before uploading (no active verification required)
|
|
813
|
+
*/
|
|
814
|
+
async checkDocumentQuality(imageData, documentType) {
|
|
815
|
+
return this.apiClient.checkDocumentQuality(imageData, documentType);
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Upload document image
|
|
819
|
+
*/
|
|
820
|
+
async uploadDocument(imageData, side, documentType) {
|
|
821
|
+
if (!this.currentVerification) {
|
|
822
|
+
throw new KoraError("INVALID_VERIFICATION_STATE" /* INVALID_VERIFICATION_STATE */, "No active verification");
|
|
823
|
+
}
|
|
824
|
+
const response = await this.apiClient.uploadDocument(
|
|
825
|
+
this.currentVerification.id,
|
|
826
|
+
imageData,
|
|
827
|
+
side,
|
|
828
|
+
documentType
|
|
829
|
+
);
|
|
830
|
+
return {
|
|
831
|
+
success: response.success,
|
|
832
|
+
qualityIssues: response.qualityIssues?.map((q) => q.message)
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Upload selfie image
|
|
837
|
+
*/
|
|
838
|
+
async uploadSelfie(imageData) {
|
|
839
|
+
if (!this.currentVerification) {
|
|
840
|
+
throw new KoraError("INVALID_VERIFICATION_STATE" /* INVALID_VERIFICATION_STATE */, "No active verification");
|
|
841
|
+
}
|
|
842
|
+
const response = await this.apiClient.uploadSelfie(this.currentVerification.id, imageData);
|
|
843
|
+
return {
|
|
844
|
+
success: response.success,
|
|
845
|
+
qualityIssues: response.qualityIssues?.map((q) => q.message)
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Start liveness session
|
|
850
|
+
*/
|
|
851
|
+
async startLivenessSession() {
|
|
852
|
+
if (!this.currentVerification) {
|
|
853
|
+
throw new KoraError("INVALID_VERIFICATION_STATE" /* INVALID_VERIFICATION_STATE */, "No active verification");
|
|
854
|
+
}
|
|
855
|
+
this.livenessSession = await this.apiClient.createLivenessSession(this.currentVerification.id);
|
|
856
|
+
return this.livenessSession;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Submit liveness challenge
|
|
860
|
+
*/
|
|
861
|
+
async submitLivenessChallenge(challenge, imageData) {
|
|
862
|
+
if (!this.currentVerification) {
|
|
863
|
+
throw new KoraError("INVALID_VERIFICATION_STATE" /* INVALID_VERIFICATION_STATE */, "No active verification");
|
|
864
|
+
}
|
|
865
|
+
const response = await this.apiClient.submitLivenessChallenge(
|
|
866
|
+
this.currentVerification.id,
|
|
867
|
+
challenge,
|
|
868
|
+
imageData
|
|
869
|
+
);
|
|
870
|
+
return {
|
|
871
|
+
passed: response.challengePassed,
|
|
872
|
+
remainingChallenges: response.remainingChallenges
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Complete the verification
|
|
877
|
+
*/
|
|
878
|
+
async completeVerification() {
|
|
879
|
+
if (!this.currentVerification) {
|
|
880
|
+
throw new KoraError("INVALID_VERIFICATION_STATE" /* INVALID_VERIFICATION_STATE */, "No active verification");
|
|
881
|
+
}
|
|
882
|
+
this.currentVerification = await this.apiClient.completeVerification(this.currentVerification.id);
|
|
883
|
+
return this.currentVerification;
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Get current verification
|
|
887
|
+
*/
|
|
888
|
+
getCurrentVerification() {
|
|
889
|
+
return this.currentVerification;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Get current liveness session
|
|
893
|
+
*/
|
|
894
|
+
getLivenessSession() {
|
|
895
|
+
return this.livenessSession;
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Check if session has timed out
|
|
899
|
+
*/
|
|
900
|
+
isSessionTimedOut() {
|
|
901
|
+
if (!this.sessionStartTime) return false;
|
|
902
|
+
const elapsed = Date.now() - this.sessionStartTime.getTime();
|
|
903
|
+
return elapsed > this.configuration.timeout * 1e3;
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Reset the session
|
|
907
|
+
*/
|
|
908
|
+
reset() {
|
|
909
|
+
this.currentVerification = null;
|
|
910
|
+
this.livenessSession = null;
|
|
911
|
+
this.sessionStartTime = null;
|
|
912
|
+
}
|
|
913
|
+
determineStepFromStatus(status) {
|
|
914
|
+
switch (status) {
|
|
915
|
+
case "pending":
|
|
916
|
+
return "consent";
|
|
917
|
+
case "document_required":
|
|
918
|
+
return "document_selection";
|
|
919
|
+
case "selfie_required":
|
|
920
|
+
return "selfie";
|
|
921
|
+
case "liveness_required":
|
|
922
|
+
return "liveness";
|
|
923
|
+
case "processing":
|
|
924
|
+
return "processing";
|
|
925
|
+
case "approved":
|
|
926
|
+
case "rejected":
|
|
927
|
+
case "review_required":
|
|
928
|
+
case "expired":
|
|
929
|
+
return "complete";
|
|
930
|
+
default:
|
|
931
|
+
return "consent";
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
KoraIDV.VERSION = "1.5.0";
|
|
936
|
+
|
|
937
|
+
// src/utils/QualityValidator.ts
|
|
938
|
+
var defaultThresholds = {
|
|
939
|
+
minBlurScore: 100,
|
|
940
|
+
minBrightness: 0.3,
|
|
941
|
+
maxBrightness: 0.85,
|
|
942
|
+
maxGlarePercentage: 0.05,
|
|
943
|
+
minFaceSizePercentage: 0.2,
|
|
944
|
+
minFaceConfidence: 0.7
|
|
945
|
+
};
|
|
946
|
+
var QualityValidator = class {
|
|
947
|
+
constructor(thresholds = {}) {
|
|
948
|
+
this.thresholds = { ...defaultThresholds, ...thresholds };
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Validate document image quality
|
|
952
|
+
*/
|
|
953
|
+
async validateDocumentImage(imageData) {
|
|
954
|
+
const issues = [];
|
|
955
|
+
const metrics = {};
|
|
956
|
+
const blurScore = this.calculateBlurScore(imageData);
|
|
957
|
+
metrics.blurScore = blurScore;
|
|
958
|
+
if (blurScore < this.thresholds.minBlurScore) {
|
|
959
|
+
issues.push({
|
|
960
|
+
type: "blur",
|
|
961
|
+
message: "Image is too blurry. Hold the device steady.",
|
|
962
|
+
severity: "error"
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
const brightness = this.calculateBrightness(imageData);
|
|
966
|
+
metrics.brightness = brightness;
|
|
967
|
+
if (brightness < this.thresholds.minBrightness) {
|
|
968
|
+
issues.push({
|
|
969
|
+
type: "too_dark",
|
|
970
|
+
message: "Image is too dark. Move to a brighter area.",
|
|
971
|
+
severity: "error"
|
|
972
|
+
});
|
|
973
|
+
} else if (brightness > this.thresholds.maxBrightness) {
|
|
974
|
+
issues.push({
|
|
975
|
+
type: "too_bright",
|
|
976
|
+
message: "Image is too bright. Reduce lighting.",
|
|
977
|
+
severity: "warning"
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
const glarePercentage = this.calculateGlarePercentage(imageData);
|
|
981
|
+
metrics.glarePercentage = glarePercentage;
|
|
982
|
+
if (glarePercentage > this.thresholds.maxGlarePercentage) {
|
|
983
|
+
issues.push({
|
|
984
|
+
type: "glare",
|
|
985
|
+
message: "Glare detected. Adjust angle to reduce reflections.",
|
|
986
|
+
severity: "warning"
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
return {
|
|
990
|
+
isValid: !issues.some((issue) => issue.severity === "error"),
|
|
991
|
+
issues,
|
|
992
|
+
metrics
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Validate selfie image quality
|
|
997
|
+
*/
|
|
998
|
+
async validateSelfieImage(imageData, faceDetection) {
|
|
999
|
+
const issues = [];
|
|
1000
|
+
const metrics = {};
|
|
1001
|
+
const blurScore = this.calculateBlurScore(imageData);
|
|
1002
|
+
metrics.blurScore = blurScore;
|
|
1003
|
+
if (blurScore < this.thresholds.minBlurScore) {
|
|
1004
|
+
issues.push({
|
|
1005
|
+
type: "blur",
|
|
1006
|
+
message: "Image is too blurry. Hold the device steady.",
|
|
1007
|
+
severity: "error"
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
const brightness = this.calculateBrightness(imageData);
|
|
1011
|
+
metrics.brightness = brightness;
|
|
1012
|
+
if (brightness < this.thresholds.minBrightness) {
|
|
1013
|
+
issues.push({
|
|
1014
|
+
type: "too_dark",
|
|
1015
|
+
message: "Image is too dark. Move to a brighter area.",
|
|
1016
|
+
severity: "error"
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
if (!faceDetection) {
|
|
1020
|
+
issues.push({
|
|
1021
|
+
type: "face_not_detected",
|
|
1022
|
+
message: "Face not detected. Position your face in the frame.",
|
|
1023
|
+
severity: "error"
|
|
1024
|
+
});
|
|
1025
|
+
} else {
|
|
1026
|
+
metrics.faceConfidence = faceDetection.confidence;
|
|
1027
|
+
if (faceDetection.confidence < this.thresholds.minFaceConfidence) {
|
|
1028
|
+
issues.push({
|
|
1029
|
+
type: "face_not_detected",
|
|
1030
|
+
message: "Face not clearly visible. Ensure good lighting.",
|
|
1031
|
+
severity: "warning"
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
const frameArea = imageData.width * imageData.height;
|
|
1035
|
+
const faceArea = faceDetection.boundingBox.width * faceDetection.boundingBox.height;
|
|
1036
|
+
const faceSizePercentage = faceArea / frameArea;
|
|
1037
|
+
metrics.faceSize = faceSizePercentage;
|
|
1038
|
+
if (faceSizePercentage < this.thresholds.minFaceSizePercentage) {
|
|
1039
|
+
issues.push({
|
|
1040
|
+
type: "face_too_small",
|
|
1041
|
+
message: "Face is too small. Move closer to the camera.",
|
|
1042
|
+
severity: "error"
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
const faceCenterX = faceDetection.boundingBox.x + faceDetection.boundingBox.width / 2;
|
|
1046
|
+
const faceCenterY = faceDetection.boundingBox.y + faceDetection.boundingBox.height / 2;
|
|
1047
|
+
const frameCenterX = imageData.width / 2;
|
|
1048
|
+
const frameCenterY = imageData.height / 2;
|
|
1049
|
+
const offsetX = (faceCenterX - frameCenterX) / imageData.width;
|
|
1050
|
+
const offsetY = (faceCenterY - frameCenterY) / imageData.height;
|
|
1051
|
+
metrics.faceCenterOffset = { x: offsetX, y: offsetY };
|
|
1052
|
+
if (Math.abs(offsetX) > 0.2 || Math.abs(offsetY) > 0.2) {
|
|
1053
|
+
issues.push({
|
|
1054
|
+
type: "face_off_center",
|
|
1055
|
+
message: "Center your face in the frame.",
|
|
1056
|
+
severity: "warning"
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return {
|
|
1061
|
+
isValid: !issues.some((issue) => issue.severity === "error"),
|
|
1062
|
+
issues,
|
|
1063
|
+
metrics
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Calculate blur score using Laplacian variance
|
|
1068
|
+
*/
|
|
1069
|
+
calculateBlurScore(imageData) {
|
|
1070
|
+
const { data, width, height } = imageData;
|
|
1071
|
+
const grayscale = new Float32Array(width * height);
|
|
1072
|
+
for (let i = 0; i < width * height; i++) {
|
|
1073
|
+
const idx = i * 4;
|
|
1074
|
+
grayscale[i] = 0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2];
|
|
1075
|
+
}
|
|
1076
|
+
const laplacian = new Float32Array(width * height);
|
|
1077
|
+
const kernel = [0, 1, 0, 1, -4, 1, 0, 1, 0];
|
|
1078
|
+
for (let y = 1; y < height - 1; y++) {
|
|
1079
|
+
for (let x = 1; x < width - 1; x++) {
|
|
1080
|
+
let sum = 0;
|
|
1081
|
+
for (let ky = -1; ky <= 1; ky++) {
|
|
1082
|
+
for (let kx = -1; kx <= 1; kx++) {
|
|
1083
|
+
const idx = (y + ky) * width + (x + kx);
|
|
1084
|
+
const kidx = (ky + 1) * 3 + (kx + 1);
|
|
1085
|
+
sum += grayscale[idx] * kernel[kidx];
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
laplacian[y * width + x] = sum;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
let mean = 0;
|
|
1092
|
+
for (let i = 0; i < laplacian.length; i++) {
|
|
1093
|
+
mean += laplacian[i];
|
|
1094
|
+
}
|
|
1095
|
+
mean /= laplacian.length;
|
|
1096
|
+
let variance = 0;
|
|
1097
|
+
for (let i = 0; i < laplacian.length; i++) {
|
|
1098
|
+
variance += Math.pow(laplacian[i] - mean, 2);
|
|
1099
|
+
}
|
|
1100
|
+
variance /= laplacian.length;
|
|
1101
|
+
return variance;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Calculate average brightness (0-1)
|
|
1105
|
+
*/
|
|
1106
|
+
calculateBrightness(imageData) {
|
|
1107
|
+
const { data } = imageData;
|
|
1108
|
+
let totalBrightness = 0;
|
|
1109
|
+
const pixelCount = data.length / 4;
|
|
1110
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
1111
|
+
const brightness = (0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]) / 255;
|
|
1112
|
+
totalBrightness += brightness;
|
|
1113
|
+
}
|
|
1114
|
+
return totalBrightness / pixelCount;
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Calculate percentage of overexposed pixels (glare)
|
|
1118
|
+
*/
|
|
1119
|
+
calculateGlarePercentage(imageData) {
|
|
1120
|
+
const { data } = imageData;
|
|
1121
|
+
let glarePixels = 0;
|
|
1122
|
+
const pixelCount = data.length / 4;
|
|
1123
|
+
const glareThreshold = 250;
|
|
1124
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
1125
|
+
if (data[i] > glareThreshold && data[i + 1] > glareThreshold && data[i + 2] > glareThreshold) {
|
|
1126
|
+
glarePixels++;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return glarePixels / pixelCount;
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
// src/utils/MrzParser.ts
|
|
1134
|
+
var MrzParser = class {
|
|
1135
|
+
/**
|
|
1136
|
+
* Parse MRZ text
|
|
1137
|
+
*/
|
|
1138
|
+
parse(mrzText) {
|
|
1139
|
+
const lines = this.cleanMrzText(mrzText);
|
|
1140
|
+
if (!lines) {
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
const format = this.detectFormat(lines);
|
|
1144
|
+
if (!format) {
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
switch (format) {
|
|
1148
|
+
case "TD1":
|
|
1149
|
+
return this.parseTD1(lines);
|
|
1150
|
+
case "TD2":
|
|
1151
|
+
return this.parseTD2(lines);
|
|
1152
|
+
case "TD3":
|
|
1153
|
+
return this.parseTD3(lines);
|
|
1154
|
+
default:
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Clean and normalize MRZ text
|
|
1160
|
+
*/
|
|
1161
|
+
cleanMrzText(text) {
|
|
1162
|
+
let cleaned = text.toUpperCase().replace(/O/g, "0").replace(/\s+/g, "").replace(/[^A-Z0-9<]/g, "");
|
|
1163
|
+
const lines = [];
|
|
1164
|
+
const lineLength = this.detectLineLength(cleaned);
|
|
1165
|
+
if (!lineLength) {
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
for (let i = 0; i < cleaned.length; i += lineLength) {
|
|
1169
|
+
lines.push(cleaned.substring(i, i + lineLength));
|
|
1170
|
+
}
|
|
1171
|
+
return lines;
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Detect MRZ line length
|
|
1175
|
+
*/
|
|
1176
|
+
detectLineLength(text) {
|
|
1177
|
+
const length = text.length;
|
|
1178
|
+
if (length >= 88 && length <= 92) return 30;
|
|
1179
|
+
if (length >= 70 && length <= 74) return 36;
|
|
1180
|
+
if (length >= 86 && length <= 90) return 44;
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Detect MRZ format
|
|
1185
|
+
*/
|
|
1186
|
+
detectFormat(lines) {
|
|
1187
|
+
if (lines.length === 3 && lines[0].length === 30) return "TD1";
|
|
1188
|
+
if (lines.length === 2 && lines[0].length === 36) return "TD2";
|
|
1189
|
+
if (lines.length === 2 && lines[0].length === 44) return "TD3";
|
|
1190
|
+
return null;
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Parse TD1 format (ID cards - 3 lines × 30 chars)
|
|
1194
|
+
*/
|
|
1195
|
+
parseTD1(lines) {
|
|
1196
|
+
const validationErrors = [];
|
|
1197
|
+
const documentType = lines[0].substring(0, 2).replace(/</g, "");
|
|
1198
|
+
const issuingCountry = lines[0].substring(2, 5);
|
|
1199
|
+
const documentNumber = lines[0].substring(5, 14).replace(/</g, "");
|
|
1200
|
+
const documentNumberCheck = lines[0].charAt(14);
|
|
1201
|
+
const optionalData1 = lines[0].substring(15, 30).replace(/</g, "") || void 0;
|
|
1202
|
+
const dateOfBirth = lines[1].substring(0, 6);
|
|
1203
|
+
const dobCheck = lines[1].charAt(6);
|
|
1204
|
+
const sex = lines[1].charAt(7);
|
|
1205
|
+
const expirationDate = lines[1].substring(8, 14);
|
|
1206
|
+
const expirationCheck = lines[1].charAt(14);
|
|
1207
|
+
const nationality = lines[1].substring(15, 18);
|
|
1208
|
+
const optionalData2 = lines[1].substring(18, 29).replace(/</g, "") || void 0;
|
|
1209
|
+
const nameParts = this.parseName(lines[2]);
|
|
1210
|
+
if (!this.validateCheckDigit(documentNumber, documentNumberCheck)) {
|
|
1211
|
+
validationErrors.push("Invalid document number check digit");
|
|
1212
|
+
}
|
|
1213
|
+
if (!this.validateCheckDigit(dateOfBirth, dobCheck)) {
|
|
1214
|
+
validationErrors.push("Invalid date of birth check digit");
|
|
1215
|
+
}
|
|
1216
|
+
if (!this.validateCheckDigit(expirationDate, expirationCheck)) {
|
|
1217
|
+
validationErrors.push("Invalid expiration date check digit");
|
|
1218
|
+
}
|
|
1219
|
+
return {
|
|
1220
|
+
format: "TD1",
|
|
1221
|
+
documentType,
|
|
1222
|
+
issuingCountry,
|
|
1223
|
+
lastName: nameParts.lastName,
|
|
1224
|
+
firstName: nameParts.firstName,
|
|
1225
|
+
documentNumber,
|
|
1226
|
+
nationality,
|
|
1227
|
+
dateOfBirth,
|
|
1228
|
+
sex,
|
|
1229
|
+
expirationDate,
|
|
1230
|
+
optionalData1,
|
|
1231
|
+
optionalData2,
|
|
1232
|
+
isValid: validationErrors.length === 0,
|
|
1233
|
+
validationErrors
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Parse TD2 format (Some ID cards - 2 lines × 36 chars)
|
|
1238
|
+
*/
|
|
1239
|
+
parseTD2(lines) {
|
|
1240
|
+
const validationErrors = [];
|
|
1241
|
+
const documentType = lines[0].substring(0, 2).replace(/</g, "");
|
|
1242
|
+
const issuingCountry = lines[0].substring(2, 5);
|
|
1243
|
+
const nameParts = this.parseName(lines[0].substring(5, 36));
|
|
1244
|
+
const documentNumber = lines[1].substring(0, 9).replace(/</g, "");
|
|
1245
|
+
const documentNumberCheck = lines[1].charAt(9);
|
|
1246
|
+
const nationality = lines[1].substring(10, 13);
|
|
1247
|
+
const dateOfBirth = lines[1].substring(13, 19);
|
|
1248
|
+
const dobCheck = lines[1].charAt(19);
|
|
1249
|
+
const sex = lines[1].charAt(20);
|
|
1250
|
+
const expirationDate = lines[1].substring(21, 27);
|
|
1251
|
+
const expirationCheck = lines[1].charAt(27);
|
|
1252
|
+
const optionalData1 = lines[1].substring(28, 35).replace(/</g, "") || void 0;
|
|
1253
|
+
if (!this.validateCheckDigit(documentNumber, documentNumberCheck)) {
|
|
1254
|
+
validationErrors.push("Invalid document number check digit");
|
|
1255
|
+
}
|
|
1256
|
+
if (!this.validateCheckDigit(dateOfBirth, dobCheck)) {
|
|
1257
|
+
validationErrors.push("Invalid date of birth check digit");
|
|
1258
|
+
}
|
|
1259
|
+
if (!this.validateCheckDigit(expirationDate, expirationCheck)) {
|
|
1260
|
+
validationErrors.push("Invalid expiration date check digit");
|
|
1261
|
+
}
|
|
1262
|
+
return {
|
|
1263
|
+
format: "TD2",
|
|
1264
|
+
documentType,
|
|
1265
|
+
issuingCountry,
|
|
1266
|
+
lastName: nameParts.lastName,
|
|
1267
|
+
firstName: nameParts.firstName,
|
|
1268
|
+
documentNumber,
|
|
1269
|
+
nationality,
|
|
1270
|
+
dateOfBirth,
|
|
1271
|
+
sex,
|
|
1272
|
+
expirationDate,
|
|
1273
|
+
optionalData1,
|
|
1274
|
+
isValid: validationErrors.length === 0,
|
|
1275
|
+
validationErrors
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Parse TD3 format (Passports - 2 lines × 44 chars)
|
|
1280
|
+
*/
|
|
1281
|
+
parseTD3(lines) {
|
|
1282
|
+
const validationErrors = [];
|
|
1283
|
+
const documentType = lines[0].substring(0, 2).replace(/</g, "");
|
|
1284
|
+
const issuingCountry = lines[0].substring(2, 5);
|
|
1285
|
+
const nameParts = this.parseName(lines[0].substring(5, 44));
|
|
1286
|
+
const documentNumber = lines[1].substring(0, 9).replace(/</g, "");
|
|
1287
|
+
const documentNumberCheck = lines[1].charAt(9);
|
|
1288
|
+
const nationality = lines[1].substring(10, 13);
|
|
1289
|
+
const dateOfBirth = lines[1].substring(13, 19);
|
|
1290
|
+
const dobCheck = lines[1].charAt(19);
|
|
1291
|
+
const sex = lines[1].charAt(20);
|
|
1292
|
+
const expirationDate = lines[1].substring(21, 27);
|
|
1293
|
+
const expirationCheck = lines[1].charAt(27);
|
|
1294
|
+
const optionalData1 = lines[1].substring(28, 42).replace(/</g, "") || void 0;
|
|
1295
|
+
if (!this.validateCheckDigit(documentNumber, documentNumberCheck)) {
|
|
1296
|
+
validationErrors.push("Invalid document number check digit");
|
|
1297
|
+
}
|
|
1298
|
+
if (!this.validateCheckDigit(dateOfBirth, dobCheck)) {
|
|
1299
|
+
validationErrors.push("Invalid date of birth check digit");
|
|
1300
|
+
}
|
|
1301
|
+
if (!this.validateCheckDigit(expirationDate, expirationCheck)) {
|
|
1302
|
+
validationErrors.push("Invalid expiration date check digit");
|
|
1303
|
+
}
|
|
1304
|
+
return {
|
|
1305
|
+
format: "TD3",
|
|
1306
|
+
documentType,
|
|
1307
|
+
issuingCountry,
|
|
1308
|
+
lastName: nameParts.lastName,
|
|
1309
|
+
firstName: nameParts.firstName,
|
|
1310
|
+
documentNumber,
|
|
1311
|
+
nationality,
|
|
1312
|
+
dateOfBirth,
|
|
1313
|
+
sex,
|
|
1314
|
+
expirationDate,
|
|
1315
|
+
optionalData1,
|
|
1316
|
+
isValid: validationErrors.length === 0,
|
|
1317
|
+
validationErrors
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Parse name field
|
|
1322
|
+
*/
|
|
1323
|
+
parseName(nameField) {
|
|
1324
|
+
const parts = nameField.split("<<");
|
|
1325
|
+
const lastName = parts[0]?.replace(/</g, " ").trim() || "";
|
|
1326
|
+
const firstName = parts[1]?.replace(/</g, " ").trim() || "";
|
|
1327
|
+
return { lastName, firstName };
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Validate MRZ check digit
|
|
1331
|
+
*/
|
|
1332
|
+
validateCheckDigit(data, checkDigit) {
|
|
1333
|
+
const weights = [7, 3, 1];
|
|
1334
|
+
let sum = 0;
|
|
1335
|
+
for (let i = 0; i < data.length; i++) {
|
|
1336
|
+
const char = data.charAt(i);
|
|
1337
|
+
let value;
|
|
1338
|
+
if (char >= "0" && char <= "9") {
|
|
1339
|
+
value = parseInt(char, 10);
|
|
1340
|
+
} else if (char >= "A" && char <= "Z") {
|
|
1341
|
+
value = char.charCodeAt(0) - 55;
|
|
1342
|
+
} else if (char === "<") {
|
|
1343
|
+
value = 0;
|
|
1344
|
+
} else {
|
|
1345
|
+
return false;
|
|
1346
|
+
}
|
|
1347
|
+
sum += value * weights[i % 3];
|
|
1348
|
+
}
|
|
1349
|
+
const expected = sum % 10;
|
|
1350
|
+
const actual = checkDigit === "<" ? 0 : parseInt(checkDigit, 10);
|
|
1351
|
+
return expected === actual;
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Format date from YYMMDD to human readable
|
|
1355
|
+
*/
|
|
1356
|
+
static formatDate(yymmdd) {
|
|
1357
|
+
if (yymmdd.length !== 6) return yymmdd;
|
|
1358
|
+
const yy = parseInt(yymmdd.substring(0, 2), 10);
|
|
1359
|
+
const mm = yymmdd.substring(2, 4);
|
|
1360
|
+
const dd = yymmdd.substring(4, 6);
|
|
1361
|
+
const year = yy <= 30 ? 2e3 + yy : 1900 + yy;
|
|
1362
|
+
return `${year}-${mm}-${dd}`;
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
// src/capture/BarcodeScanner.ts
|
|
1367
|
+
var WebBarcodeScanner = class _WebBarcodeScanner {
|
|
1368
|
+
/**
|
|
1369
|
+
* Construct a scanner. Restricts to PDF417 by default to avoid false
|
|
1370
|
+
* positives on the small Code128 strip US DLs also carry. Callers
|
|
1371
|
+
* onboarding QR-based docs (Nigeria voter's card) should pass
|
|
1372
|
+
* `['pdf417', 'qr_code']`.
|
|
1373
|
+
*/
|
|
1374
|
+
constructor(formats = ["pdf417"]) {
|
|
1375
|
+
this.detector = null;
|
|
1376
|
+
if (_WebBarcodeScanner.isSupported()) {
|
|
1377
|
+
const Ctor = globalThis.BarcodeDetector;
|
|
1378
|
+
try {
|
|
1379
|
+
this.detector = new Ctor({ formats });
|
|
1380
|
+
} catch {
|
|
1381
|
+
this.detector = new Ctor();
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Whether the current environment has a usable BarcodeDetector.
|
|
1387
|
+
* Either native (modern Chromium / Samsung) or a polyfill.
|
|
1388
|
+
*/
|
|
1389
|
+
static isSupported() {
|
|
1390
|
+
return typeof globalThis !== "undefined" && typeof globalThis.BarcodeDetector === "function";
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Attempt to decode a PDF417 barcode from the supplied image source.
|
|
1394
|
+
* Accepts any `ImageBitmapSource`: Blob, HTMLImageElement,
|
|
1395
|
+
* HTMLCanvasElement, ImageBitmap, OffscreenCanvas.
|
|
1396
|
+
*
|
|
1397
|
+
* Returns the raw AAMVA payload as a single string (newline-separated
|
|
1398
|
+
* records, exactly the form the server's AAMVA parser expects) or
|
|
1399
|
+
* `null` when no barcode was found, decoding failed, or the API is
|
|
1400
|
+
* unavailable.
|
|
1401
|
+
*/
|
|
1402
|
+
async decodePdf417(source) {
|
|
1403
|
+
if (!this.detector) return null;
|
|
1404
|
+
try {
|
|
1405
|
+
const results = await this.detector.detect(source);
|
|
1406
|
+
for (const r of results) {
|
|
1407
|
+
if (r.rawValue && r.rawValue.length > 0) {
|
|
1408
|
+
return r.rawValue;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return null;
|
|
1412
|
+
} catch {
|
|
1413
|
+
return null;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
|
|
1418
|
+
// src/wallet/WalletModels.ts
|
|
1419
|
+
var WalletError = class _WalletError extends Error {
|
|
1420
|
+
constructor(code, message) {
|
|
1421
|
+
super(message);
|
|
1422
|
+
this.code = code;
|
|
1423
|
+
this.name = "WalletError";
|
|
1424
|
+
}
|
|
1425
|
+
static storageFailed() {
|
|
1426
|
+
return new _WalletError("STORAGE_FAILED", "Failed to store credential.");
|
|
1427
|
+
}
|
|
1428
|
+
static credentialNotFound() {
|
|
1429
|
+
return new _WalletError("CREDENTIAL_NOT_FOUND", "Credential not found.");
|
|
1430
|
+
}
|
|
1431
|
+
static credentialExpired() {
|
|
1432
|
+
return new _WalletError("CREDENTIAL_EXPIRED", "Credential has expired.");
|
|
1433
|
+
}
|
|
1434
|
+
static encodingFailed() {
|
|
1435
|
+
return new _WalletError(
|
|
1436
|
+
"ENCODING_FAILED",
|
|
1437
|
+
"Failed to encode credential data."
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
static cryptoUnavailable() {
|
|
1441
|
+
return new _WalletError(
|
|
1442
|
+
"CRYPTO_UNAVAILABLE",
|
|
1443
|
+
"Web Crypto API is not available in this environment."
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
function createWalletCredential(params) {
|
|
1448
|
+
return {
|
|
1449
|
+
"@context": params["@context"] ?? ["https://www.w3.org/ns/credentials/v2"],
|
|
1450
|
+
type: params.type ?? [
|
|
1451
|
+
"VerifiableCredential",
|
|
1452
|
+
"KoraIdentityCredential"
|
|
1453
|
+
],
|
|
1454
|
+
...params
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// src/wallet/CredentialStore.ts
|
|
1459
|
+
var DB_NAME = "kora_wallet";
|
|
1460
|
+
var DB_VERSION = 1;
|
|
1461
|
+
var STORE_NAME = "credentials";
|
|
1462
|
+
var KEY_STORE_NAME = "crypto_keys";
|
|
1463
|
+
var ENCRYPTION_KEY_ID = "wallet_master_key";
|
|
1464
|
+
var WalletCredentialStore = class {
|
|
1465
|
+
constructor() {
|
|
1466
|
+
this.db = null;
|
|
1467
|
+
this.cryptoKey = null;
|
|
1468
|
+
}
|
|
1469
|
+
// MARK: - Database Initialization
|
|
1470
|
+
async getDb() {
|
|
1471
|
+
if (this.db) return this.db;
|
|
1472
|
+
return new Promise((resolve, reject) => {
|
|
1473
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
1474
|
+
request.onupgradeneeded = () => {
|
|
1475
|
+
const db = request.result;
|
|
1476
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
1477
|
+
db.createObjectStore(STORE_NAME, { keyPath: "id" });
|
|
1478
|
+
}
|
|
1479
|
+
if (!db.objectStoreNames.contains(KEY_STORE_NAME)) {
|
|
1480
|
+
db.createObjectStore(KEY_STORE_NAME, { keyPath: "id" });
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
request.onsuccess = () => {
|
|
1484
|
+
this.db = request.result;
|
|
1485
|
+
resolve(this.db);
|
|
1486
|
+
};
|
|
1487
|
+
request.onerror = () => {
|
|
1488
|
+
reject(WalletError.storageFailed());
|
|
1489
|
+
};
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
// MARK: - Crypto Key Management
|
|
1493
|
+
async getCryptoKey() {
|
|
1494
|
+
if (this.cryptoKey) return this.cryptoKey;
|
|
1495
|
+
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
1496
|
+
throw WalletError.cryptoUnavailable();
|
|
1497
|
+
}
|
|
1498
|
+
const db = await this.getDb();
|
|
1499
|
+
const existingKey = await new Promise(
|
|
1500
|
+
(resolve, reject) => {
|
|
1501
|
+
const tx = db.transaction(KEY_STORE_NAME, "readonly");
|
|
1502
|
+
const store = tx.objectStore(KEY_STORE_NAME);
|
|
1503
|
+
const request = store.get(ENCRYPTION_KEY_ID);
|
|
1504
|
+
request.onsuccess = () => {
|
|
1505
|
+
const record = request.result;
|
|
1506
|
+
if (record?.key) {
|
|
1507
|
+
crypto.subtle.importKey("raw", record.key, { name: "AES-GCM" }, false, [
|
|
1508
|
+
"encrypt",
|
|
1509
|
+
"decrypt"
|
|
1510
|
+
]).then(resolve).catch(() => resolve(null));
|
|
1511
|
+
} else {
|
|
1512
|
+
resolve(null);
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
request.onerror = () => reject(WalletError.storageFailed());
|
|
1516
|
+
}
|
|
1517
|
+
);
|
|
1518
|
+
if (existingKey) {
|
|
1519
|
+
this.cryptoKey = existingKey;
|
|
1520
|
+
return existingKey;
|
|
1521
|
+
}
|
|
1522
|
+
const newKey = await crypto.subtle.generateKey(
|
|
1523
|
+
{ name: "AES-GCM", length: 256 },
|
|
1524
|
+
true,
|
|
1525
|
+
["encrypt", "decrypt"]
|
|
1526
|
+
);
|
|
1527
|
+
const rawKey = await crypto.subtle.exportKey("raw", newKey);
|
|
1528
|
+
await new Promise((resolve, reject) => {
|
|
1529
|
+
const tx = db.transaction(KEY_STORE_NAME, "readwrite");
|
|
1530
|
+
const store = tx.objectStore(KEY_STORE_NAME);
|
|
1531
|
+
store.put({ id: ENCRYPTION_KEY_ID, key: rawKey });
|
|
1532
|
+
tx.oncomplete = () => resolve();
|
|
1533
|
+
tx.onerror = () => reject(WalletError.storageFailed());
|
|
1534
|
+
});
|
|
1535
|
+
this.cryptoKey = await crypto.subtle.importKey(
|
|
1536
|
+
"raw",
|
|
1537
|
+
rawKey,
|
|
1538
|
+
{ name: "AES-GCM" },
|
|
1539
|
+
false,
|
|
1540
|
+
["encrypt", "decrypt"]
|
|
1541
|
+
);
|
|
1542
|
+
return this.cryptoKey;
|
|
1543
|
+
}
|
|
1544
|
+
// MARK: - Encrypt / Decrypt
|
|
1545
|
+
async encrypt(data) {
|
|
1546
|
+
const key = await this.getCryptoKey();
|
|
1547
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
1548
|
+
const encoded = new TextEncoder().encode(data);
|
|
1549
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
1550
|
+
{ name: "AES-GCM", iv },
|
|
1551
|
+
key,
|
|
1552
|
+
encoded
|
|
1553
|
+
);
|
|
1554
|
+
const result = new Uint8Array(iv.length + ciphertext.byteLength);
|
|
1555
|
+
result.set(iv);
|
|
1556
|
+
result.set(new Uint8Array(ciphertext), iv.length);
|
|
1557
|
+
return result.buffer;
|
|
1558
|
+
}
|
|
1559
|
+
async decrypt(data) {
|
|
1560
|
+
const key = await this.getCryptoKey();
|
|
1561
|
+
const bytes = new Uint8Array(data);
|
|
1562
|
+
const iv = bytes.slice(0, 12);
|
|
1563
|
+
const ciphertext = bytes.slice(12);
|
|
1564
|
+
const plaintext = await crypto.subtle.decrypt(
|
|
1565
|
+
{ name: "AES-GCM", iv },
|
|
1566
|
+
key,
|
|
1567
|
+
ciphertext
|
|
1568
|
+
);
|
|
1569
|
+
return new TextDecoder().decode(plaintext);
|
|
1570
|
+
}
|
|
1571
|
+
// MARK: - CRUD Operations
|
|
1572
|
+
async save(id, credential) {
|
|
1573
|
+
const db = await this.getDb();
|
|
1574
|
+
const json = JSON.stringify(credential);
|
|
1575
|
+
const encrypted = await this.encrypt(json);
|
|
1576
|
+
return new Promise((resolve, reject) => {
|
|
1577
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
1578
|
+
const store = tx.objectStore(STORE_NAME);
|
|
1579
|
+
store.put({ id, data: encrypted });
|
|
1580
|
+
tx.oncomplete = () => resolve();
|
|
1581
|
+
tx.onerror = () => reject(WalletError.storageFailed());
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
async load(id) {
|
|
1585
|
+
const db = await this.getDb();
|
|
1586
|
+
const record = await new Promise(
|
|
1587
|
+
(resolve, reject) => {
|
|
1588
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
1589
|
+
const store = tx.objectStore(STORE_NAME);
|
|
1590
|
+
const request = store.get(id);
|
|
1591
|
+
request.onsuccess = () => resolve(request.result ?? null);
|
|
1592
|
+
request.onerror = () => reject(WalletError.storageFailed());
|
|
1593
|
+
}
|
|
1594
|
+
);
|
|
1595
|
+
if (!record) return null;
|
|
1596
|
+
try {
|
|
1597
|
+
const json = await this.decrypt(record.data);
|
|
1598
|
+
return JSON.parse(json);
|
|
1599
|
+
} catch {
|
|
1600
|
+
return null;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
async delete(id) {
|
|
1604
|
+
const db = await this.getDb();
|
|
1605
|
+
return new Promise((resolve, reject) => {
|
|
1606
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
1607
|
+
const store = tx.objectStore(STORE_NAME);
|
|
1608
|
+
store.delete(id);
|
|
1609
|
+
tx.oncomplete = () => resolve();
|
|
1610
|
+
tx.onerror = () => reject(WalletError.storageFailed());
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
async listIds() {
|
|
1614
|
+
const db = await this.getDb();
|
|
1615
|
+
return new Promise((resolve, reject) => {
|
|
1616
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
1617
|
+
const store = tx.objectStore(STORE_NAME);
|
|
1618
|
+
const request = store.getAllKeys();
|
|
1619
|
+
request.onsuccess = () => resolve(request.result);
|
|
1620
|
+
request.onerror = () => reject(WalletError.storageFailed());
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Close the database connection and clear cached crypto key.
|
|
1625
|
+
*/
|
|
1626
|
+
close() {
|
|
1627
|
+
this.db?.close();
|
|
1628
|
+
this.db = null;
|
|
1629
|
+
this.cryptoKey = null;
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
1632
|
+
|
|
1633
|
+
// src/wallet/SelectiveDisclosure.ts
|
|
1634
|
+
var DisclosureClaim = /* @__PURE__ */ ((DisclosureClaim2) => {
|
|
1635
|
+
DisclosureClaim2["FullName"] = "fullName";
|
|
1636
|
+
DisclosureClaim2["DateOfBirth"] = "dateOfBirth";
|
|
1637
|
+
DisclosureClaim2["Nationality"] = "nationality";
|
|
1638
|
+
DisclosureClaim2["VerificationLevel"] = "verificationLevel";
|
|
1639
|
+
DisclosureClaim2["DocumentType"] = "documentType";
|
|
1640
|
+
DisclosureClaim2["DocumentCountry"] = "documentCountry";
|
|
1641
|
+
DisclosureClaim2["BiometricMatch"] = "biometricMatch";
|
|
1642
|
+
DisclosureClaim2["LivenessCheck"] = "livenessCheck";
|
|
1643
|
+
DisclosureClaim2["GovernmentDbVerified"] = "governmentDbVerified";
|
|
1644
|
+
DisclosureClaim2["VerifiedAt"] = "verifiedAt";
|
|
1645
|
+
DisclosureClaim2["ConfidenceScore"] = "confidenceScore";
|
|
1646
|
+
return DisclosureClaim2;
|
|
1647
|
+
})(DisclosureClaim || {});
|
|
1648
|
+
var DisclosureProfiles = {
|
|
1649
|
+
full: { type: "full" },
|
|
1650
|
+
onboarding: { type: "onboarding" },
|
|
1651
|
+
ageOnly: { type: "ageOnly" },
|
|
1652
|
+
nationalityOnly: { type: "nationalityOnly" },
|
|
1653
|
+
verificationOnly: { type: "verificationOnly" },
|
|
1654
|
+
custom: (claims) => ({
|
|
1655
|
+
type: "custom",
|
|
1656
|
+
claims
|
|
1657
|
+
})
|
|
1658
|
+
};
|
|
1659
|
+
function getProfileName(profile) {
|
|
1660
|
+
return profile.type;
|
|
1661
|
+
}
|
|
1662
|
+
var ALL_CLAIMS = new Set(Object.values(DisclosureClaim));
|
|
1663
|
+
var ONBOARDING_CLAIMS = /* @__PURE__ */ new Set([
|
|
1664
|
+
"fullName" /* FullName */,
|
|
1665
|
+
"dateOfBirth" /* DateOfBirth */,
|
|
1666
|
+
"nationality" /* Nationality */,
|
|
1667
|
+
"verificationLevel" /* VerificationLevel */,
|
|
1668
|
+
"documentType" /* DocumentType */,
|
|
1669
|
+
"documentCountry" /* DocumentCountry */
|
|
1670
|
+
]);
|
|
1671
|
+
var VERIFICATION_CLAIMS = /* @__PURE__ */ new Set([
|
|
1672
|
+
"verificationLevel" /* VerificationLevel */,
|
|
1673
|
+
"verifiedAt" /* VerifiedAt */,
|
|
1674
|
+
"confidenceScore" /* ConfidenceScore */
|
|
1675
|
+
]);
|
|
1676
|
+
function getClaimsForProfile(profile) {
|
|
1677
|
+
switch (profile.type) {
|
|
1678
|
+
case "full":
|
|
1679
|
+
return ALL_CLAIMS;
|
|
1680
|
+
case "onboarding":
|
|
1681
|
+
return ONBOARDING_CLAIMS;
|
|
1682
|
+
case "ageOnly":
|
|
1683
|
+
return /* @__PURE__ */ new Set(["dateOfBirth" /* DateOfBirth */]);
|
|
1684
|
+
case "nationalityOnly":
|
|
1685
|
+
return /* @__PURE__ */ new Set(["nationality" /* Nationality */]);
|
|
1686
|
+
case "verificationOnly":
|
|
1687
|
+
return VERIFICATION_CLAIMS;
|
|
1688
|
+
case "custom":
|
|
1689
|
+
return profile.claims;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
function applyDisclosure(profile, credential) {
|
|
1693
|
+
const claims = getClaimsForProfile(profile);
|
|
1694
|
+
const subject = credential.credentialSubject;
|
|
1695
|
+
const disclosed = {
|
|
1696
|
+
id: subject.id,
|
|
1697
|
+
fullName: claims.has("fullName" /* FullName */) ? subject.fullName : "",
|
|
1698
|
+
dateOfBirth: claims.has("dateOfBirth" /* DateOfBirth */) ? subject.dateOfBirth : void 0,
|
|
1699
|
+
nationality: claims.has("nationality" /* Nationality */) ? subject.nationality : void 0,
|
|
1700
|
+
verificationLevel: claims.has("verificationLevel" /* VerificationLevel */) ? subject.verificationLevel : "",
|
|
1701
|
+
documentType: claims.has("documentType" /* DocumentType */) ? subject.documentType : "",
|
|
1702
|
+
documentCountry: claims.has("documentCountry" /* DocumentCountry */) ? subject.documentCountry : "",
|
|
1703
|
+
biometricMatch: claims.has("biometricMatch" /* BiometricMatch */) && subject.biometricMatch,
|
|
1704
|
+
livenessCheck: claims.has("livenessCheck" /* LivenessCheck */) && subject.livenessCheck,
|
|
1705
|
+
governmentDbVerified: claims.has("governmentDbVerified" /* GovernmentDbVerified */) && subject.governmentDbVerified,
|
|
1706
|
+
verifiedAt: claims.has("verifiedAt" /* VerifiedAt */) ? subject.verifiedAt : "",
|
|
1707
|
+
confidenceScore: claims.has("confidenceScore" /* ConfidenceScore */) ? subject.confidenceScore : 0
|
|
1708
|
+
};
|
|
1709
|
+
return {
|
|
1710
|
+
...credential,
|
|
1711
|
+
credentialSubject: disclosed
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
function computeAgeOver18(dateOfBirth) {
|
|
1715
|
+
if (!dateOfBirth) return false;
|
|
1716
|
+
const dob = new Date(dateOfBirth);
|
|
1717
|
+
if (isNaN(dob.getTime())) return false;
|
|
1718
|
+
const now = /* @__PURE__ */ new Date();
|
|
1719
|
+
let age = now.getFullYear() - dob.getFullYear();
|
|
1720
|
+
const monthDiff = now.getMonth() - dob.getMonth();
|
|
1721
|
+
if (monthDiff < 0 || monthDiff === 0 && now.getDate() < dob.getDate()) {
|
|
1722
|
+
age--;
|
|
1723
|
+
}
|
|
1724
|
+
return age >= 18;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// src/wallet/VerifiablePresentation.ts
|
|
1728
|
+
var WalletPresentationBuilder = {
|
|
1729
|
+
/**
|
|
1730
|
+
* Create a Verifiable Presentation from a credential with selective disclosure.
|
|
1731
|
+
*/
|
|
1732
|
+
create(params) {
|
|
1733
|
+
const disclosed = applyDisclosure(params.profile, params.credential);
|
|
1734
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1735
|
+
return {
|
|
1736
|
+
"@context": ["https://www.w3.org/ns/credentials/v2"],
|
|
1737
|
+
type: ["VerifiablePresentation"],
|
|
1738
|
+
holder: params.holder ?? null,
|
|
1739
|
+
verifiableCredential: [disclosed],
|
|
1740
|
+
created: now,
|
|
1741
|
+
audience: params.audience ?? null,
|
|
1742
|
+
challenge: params.nonce ?? null
|
|
1743
|
+
};
|
|
1744
|
+
},
|
|
1745
|
+
/**
|
|
1746
|
+
* Serialize a presentation to a JSON string.
|
|
1747
|
+
*/
|
|
1748
|
+
encode(presentation) {
|
|
1749
|
+
return JSON.stringify(presentation, null, 2);
|
|
1750
|
+
},
|
|
1751
|
+
/**
|
|
1752
|
+
* Deserialize a presentation from a JSON string.
|
|
1753
|
+
*/
|
|
1754
|
+
decode(json) {
|
|
1755
|
+
return JSON.parse(json);
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
|
|
1759
|
+
// src/wallet/KoraWallet.ts
|
|
1760
|
+
var MAX_INLINE_SIZE = 2048;
|
|
1761
|
+
var KoraWallet = class {
|
|
1762
|
+
constructor() {
|
|
1763
|
+
this.credentialStore = new WalletCredentialStore();
|
|
1764
|
+
}
|
|
1765
|
+
// MARK: - Credential Management
|
|
1766
|
+
/**
|
|
1767
|
+
* Store a Verifiable Credential in the wallet.
|
|
1768
|
+
* Returns the storage ID (same as the credential's `id`).
|
|
1769
|
+
*/
|
|
1770
|
+
async store(credential) {
|
|
1771
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1772
|
+
const stored = {
|
|
1773
|
+
id: credential.id,
|
|
1774
|
+
credential,
|
|
1775
|
+
storedAt: now,
|
|
1776
|
+
issuerDID: credential.issuer,
|
|
1777
|
+
subjectName: credential.credentialSubject.fullName,
|
|
1778
|
+
expiresAt: credential.expirationDate
|
|
1779
|
+
};
|
|
1780
|
+
await this.credentialStore.save(credential.id, stored);
|
|
1781
|
+
return credential.id;
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Retrieve all stored credentials.
|
|
1785
|
+
*/
|
|
1786
|
+
async getCredentials() {
|
|
1787
|
+
const ids = await this.credentialStore.listIds();
|
|
1788
|
+
const results = [];
|
|
1789
|
+
for (const id of ids) {
|
|
1790
|
+
const stored = await this.credentialStore.load(id);
|
|
1791
|
+
if (stored) results.push(stored);
|
|
1792
|
+
}
|
|
1793
|
+
return results;
|
|
1794
|
+
}
|
|
1795
|
+
/**
|
|
1796
|
+
* Retrieve a single credential by ID.
|
|
1797
|
+
*/
|
|
1798
|
+
async getCredential(id) {
|
|
1799
|
+
return this.credentialStore.load(id);
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Delete a credential from the wallet.
|
|
1803
|
+
*/
|
|
1804
|
+
async deleteCredential(id) {
|
|
1805
|
+
await this.credentialStore.delete(id);
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Number of credentials currently stored.
|
|
1809
|
+
*/
|
|
1810
|
+
async getCredentialCount() {
|
|
1811
|
+
const ids = await this.credentialStore.listIds();
|
|
1812
|
+
return ids.length;
|
|
1813
|
+
}
|
|
1814
|
+
// MARK: - Presentation
|
|
1815
|
+
/**
|
|
1816
|
+
* Create a Verifiable Presentation with selective disclosure.
|
|
1817
|
+
*/
|
|
1818
|
+
async createPresentation(params) {
|
|
1819
|
+
const stored = await this.credentialStore.load(params.credentialId);
|
|
1820
|
+
if (!stored) {
|
|
1821
|
+
throw WalletError.credentialNotFound();
|
|
1822
|
+
}
|
|
1823
|
+
if (await this.isExpired(params.credentialId)) {
|
|
1824
|
+
throw WalletError.credentialExpired();
|
|
1825
|
+
}
|
|
1826
|
+
return WalletPresentationBuilder.create({
|
|
1827
|
+
credential: stored.credential,
|
|
1828
|
+
profile: params.profile,
|
|
1829
|
+
audience: params.audience,
|
|
1830
|
+
nonce: params.nonce
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* Generate a deep link URL for sharing a presentation.
|
|
1835
|
+
*/
|
|
1836
|
+
generateDeepLink(presentation, profile = DisclosureProfiles.full) {
|
|
1837
|
+
const json = JSON.stringify(presentation);
|
|
1838
|
+
const data = new TextEncoder().encode(json);
|
|
1839
|
+
if (data.length <= MAX_INLINE_SIZE) {
|
|
1840
|
+
const encoded = base64UrlEncode(data);
|
|
1841
|
+
return `korastratum://present?data=${encoded}`;
|
|
1842
|
+
}
|
|
1843
|
+
const credId = presentation.verifiableCredential[0]?.id ?? "unknown";
|
|
1844
|
+
const profileName = getProfileName(profile);
|
|
1845
|
+
return `korastratum://present?ref=${credId}&profile=${profileName}`;
|
|
1846
|
+
}
|
|
1847
|
+
// MARK: - Expiry
|
|
1848
|
+
/**
|
|
1849
|
+
* Check whether a stored credential has expired.
|
|
1850
|
+
*/
|
|
1851
|
+
async isExpired(credentialId) {
|
|
1852
|
+
const stored = await this.credentialStore.load(credentialId);
|
|
1853
|
+
if (!stored) return true;
|
|
1854
|
+
const expires = new Date(stored.expiresAt);
|
|
1855
|
+
if (isNaN(expires.getTime())) return false;
|
|
1856
|
+
return /* @__PURE__ */ new Date() > expires;
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Close the store and free resources.
|
|
1860
|
+
*/
|
|
1861
|
+
close() {
|
|
1862
|
+
this.credentialStore.close();
|
|
1863
|
+
}
|
|
1864
|
+
};
|
|
1865
|
+
function base64UrlEncode(data) {
|
|
1866
|
+
const binString = Array.from(
|
|
1867
|
+
data,
|
|
1868
|
+
(byte) => String.fromCodePoint(byte)
|
|
1869
|
+
).join("");
|
|
1870
|
+
const base64 = btoa(binString);
|
|
1871
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1872
|
+
}
|
|
1873
|
+
export {
|
|
1874
|
+
ApiClient,
|
|
1875
|
+
DisclosureClaim,
|
|
1876
|
+
DisclosureProfiles,
|
|
1877
|
+
DocumentType,
|
|
1878
|
+
KoraError,
|
|
1879
|
+
KoraErrorCode,
|
|
1880
|
+
KoraIDV,
|
|
1881
|
+
KoraWallet,
|
|
1882
|
+
MrzParser,
|
|
1883
|
+
QualityValidator,
|
|
1884
|
+
WalletCredentialStore,
|
|
1885
|
+
WalletError,
|
|
1886
|
+
WalletPresentationBuilder,
|
|
1887
|
+
WebBarcodeScanner,
|
|
1888
|
+
applyDisclosure,
|
|
1889
|
+
blobToBase64,
|
|
1890
|
+
computeAgeOver18,
|
|
1891
|
+
createWalletCredential,
|
|
1892
|
+
getDocumentTypeInfo
|
|
1893
|
+
};
|