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