@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.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
+ };