@koraidv/core 1.7.0 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +73 -17
- package/dist/index.d.ts +73 -17
- package/dist/index.js +123 -65
- package/dist/index.mjs +123 -65
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -506,7 +506,24 @@ declare class KoraIDV {
|
|
|
506
506
|
}
|
|
507
507
|
|
|
508
508
|
/**
|
|
509
|
-
* API Client for Kora IDV
|
|
509
|
+
* API Client for Kora IDV.
|
|
510
|
+
*
|
|
511
|
+
* Wire contract (matches the backend and the Android/iOS SDKs):
|
|
512
|
+
*
|
|
513
|
+
* - Request bodies: JSON with camelCase keys. The earlier multipart-
|
|
514
|
+
* form-data + snake_case combo was rejected by the backend (which
|
|
515
|
+
* only parses JSON on /document, /selfie, /liveness/challenge) and
|
|
516
|
+
* diverged from the native SDKs. Surfaced 2026-05-29 by Luckycat's
|
|
517
|
+
* Web SDK integration — backend returned 400 on the very first
|
|
518
|
+
* POST because `external_id` didn't match the server's `externalId`
|
|
519
|
+
* JSON tag.
|
|
520
|
+
*
|
|
521
|
+
* - Response bodies: snake_case auto-converted to camelCase by
|
|
522
|
+
* transformResponse() below. Domain models defined in
|
|
523
|
+
* types/ApiModels.ts are the source of truth for shape.
|
|
524
|
+
*
|
|
525
|
+
* - Authentication: Bearer-style Authorization header (raw API key)
|
|
526
|
+
* + X-Tenant-ID header. No cookies.
|
|
510
527
|
*/
|
|
511
528
|
declare class ApiClient {
|
|
512
529
|
private readonly baseUrl;
|
|
@@ -515,7 +532,22 @@ declare class ApiClient {
|
|
|
515
532
|
private readonly baseDelay;
|
|
516
533
|
constructor(configuration: Configuration);
|
|
517
534
|
/**
|
|
518
|
-
* Get supported countries and their document types
|
|
535
|
+
* Get supported countries and their document types.
|
|
536
|
+
*
|
|
537
|
+
* Backend has no dedicated /supported-countries endpoint — the
|
|
538
|
+
* countries catalog is bundled into the /document-types response
|
|
539
|
+
* alongside the per-type metadata. We fetch that bundled payload,
|
|
540
|
+
* then project it onto the SDK's `SupportedCountry` shape:
|
|
541
|
+
* - `id` ← `code` (ISO-3166 alpha-2)
|
|
542
|
+
* - `flagEmoji` derived from the ISO code
|
|
543
|
+
* - `documentTypes` filtered from the bundled types list by country
|
|
544
|
+
*
|
|
545
|
+
* Mirrors the iOS / Android pattern (SessionManager.fetchSupported-
|
|
546
|
+
* Countries on iOS, ApiService.getDocumentTypes on Android). The
|
|
547
|
+
* previous standalone /supported-countries call had been silently
|
|
548
|
+
* 404-ing since the Web SDK shipped — surfaced 2026-05-29 by
|
|
549
|
+
* Luckycat's integration, hot on the heels of the v1.7.1
|
|
550
|
+
* wire-format pass.
|
|
519
551
|
*/
|
|
520
552
|
getSupportedCountries(): Promise<SupportedCountry[]>;
|
|
521
553
|
/**
|
|
@@ -529,30 +561,50 @@ declare class ApiClient {
|
|
|
529
561
|
/**
|
|
530
562
|
* Upload document image.
|
|
531
563
|
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
564
|
+
* Both sides now use the same JSON wire format as the Android/iOS SDKs.
|
|
565
|
+
* Front sends documentType + optional country; back sends optional
|
|
566
|
+
* decodedBarcodePayload (Phase 4 fast-path — when the browser's
|
|
567
|
+
* BarcodeDetector decoded the PDF417 on-device, the AAMVA payload
|
|
568
|
+
* travels here so the server can skip image-based barcode decoding).
|
|
569
|
+
*
|
|
570
|
+
* The previous front-side multipart path returned HTTP 400 on every
|
|
571
|
+
* request because the server's /document handler only parses
|
|
572
|
+
* application/json — same trap iOS hit and fixed in v1.6.0.
|
|
540
573
|
*/
|
|
541
|
-
uploadDocument(verificationId: string, imageData: Blob, side: 'front' | 'back', documentType: DocumentType, decodedBarcodePayload?: string): Promise<DocumentUploadResponse>;
|
|
574
|
+
uploadDocument(verificationId: string, imageData: Blob, side: 'front' | 'back', documentType: DocumentType, decodedBarcodePayload?: string, country?: string): Promise<DocumentUploadResponse>;
|
|
542
575
|
/**
|
|
543
|
-
* Upload selfie image
|
|
576
|
+
* Upload selfie image.
|
|
577
|
+
*
|
|
578
|
+
* JSON with imageBase64 — matches the server's UploadSelfieRequest
|
|
579
|
+
* { ImageBase64 string } and the Android/iOS wire format. Previous
|
|
580
|
+
* multipart path returned HTTP 400 since the SDK shipped.
|
|
544
581
|
*/
|
|
545
582
|
uploadSelfie(verificationId: string, imageData: Blob): Promise<SelfieUploadResponse>;
|
|
546
583
|
/**
|
|
547
|
-
* Create liveness session
|
|
584
|
+
* Create liveness session.
|
|
585
|
+
*
|
|
586
|
+
* Decodes the wire DTO (matches backend's `models.LivenessSession`
|
|
587
|
+
* JSON), then projects it onto the domain `LivenessSession` the UI
|
|
588
|
+
* expects. Backend doesn't send `sessionId`/`expiresAt`, and per-
|
|
589
|
+
* challenge `id`/`instruction`/`order` aren't on the wire — domain
|
|
590
|
+
* values are synthesized client-side. Mirrors the iOS v1.7.0 pattern
|
|
591
|
+
* at koraidv-ios/.../SessionManager.swift::createLivenessSession.
|
|
592
|
+
*
|
|
593
|
+
* Without this DTO/domain split, the SDK accessed undefined fields
|
|
594
|
+
* on the response and produced a session with sessionId=undefined,
|
|
595
|
+
* which then failed every downstream challenge submission.
|
|
548
596
|
*/
|
|
549
597
|
createLivenessSession(verificationId: string): Promise<LivenessSession>;
|
|
550
598
|
/**
|
|
551
|
-
* Submit liveness challenge
|
|
599
|
+
* Submit liveness challenge result.
|
|
600
|
+
*
|
|
601
|
+
* JSON with challengeType + imageBase64 — matches the server's
|
|
602
|
+
* SubmitLivenessChallengeRequest and the Android/iOS wire format.
|
|
603
|
+
* Previous multipart + snake_case path was double-broken.
|
|
552
604
|
*/
|
|
553
605
|
submitLivenessChallenge(verificationId: string, challenge: LivenessChallenge, imageData: Blob): Promise<LivenessChallengeResponse>;
|
|
554
606
|
/**
|
|
555
|
-
* Check document quality before uploading
|
|
607
|
+
* Check document quality before uploading.
|
|
556
608
|
*/
|
|
557
609
|
checkDocumentQuality(imageData: Blob, documentType: string): Promise<DocumentQualityResponse>;
|
|
558
610
|
/**
|
|
@@ -583,9 +635,13 @@ declare class ApiClient {
|
|
|
583
635
|
private shouldRetryNetworkError;
|
|
584
636
|
private calculateDelay;
|
|
585
637
|
private sleep;
|
|
586
|
-
private blobToBase64;
|
|
587
638
|
/**
|
|
588
|
-
* Transform snake_case response to camelCase
|
|
639
|
+
* Transform snake_case response to camelCase.
|
|
640
|
+
*
|
|
641
|
+
* Some backend endpoints still return snake_case keys; the
|
|
642
|
+
* domain types in ApiModels.ts are camelCase. This walker turns
|
|
643
|
+
* `external_id` → `externalId` etc. recursively. Safe no-op for
|
|
644
|
+
* already-camelCase responses.
|
|
589
645
|
*/
|
|
590
646
|
private transformResponse;
|
|
591
647
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -506,7 +506,24 @@ declare class KoraIDV {
|
|
|
506
506
|
}
|
|
507
507
|
|
|
508
508
|
/**
|
|
509
|
-
* API Client for Kora IDV
|
|
509
|
+
* API Client for Kora IDV.
|
|
510
|
+
*
|
|
511
|
+
* Wire contract (matches the backend and the Android/iOS SDKs):
|
|
512
|
+
*
|
|
513
|
+
* - Request bodies: JSON with camelCase keys. The earlier multipart-
|
|
514
|
+
* form-data + snake_case combo was rejected by the backend (which
|
|
515
|
+
* only parses JSON on /document, /selfie, /liveness/challenge) and
|
|
516
|
+
* diverged from the native SDKs. Surfaced 2026-05-29 by Luckycat's
|
|
517
|
+
* Web SDK integration — backend returned 400 on the very first
|
|
518
|
+
* POST because `external_id` didn't match the server's `externalId`
|
|
519
|
+
* JSON tag.
|
|
520
|
+
*
|
|
521
|
+
* - Response bodies: snake_case auto-converted to camelCase by
|
|
522
|
+
* transformResponse() below. Domain models defined in
|
|
523
|
+
* types/ApiModels.ts are the source of truth for shape.
|
|
524
|
+
*
|
|
525
|
+
* - Authentication: Bearer-style Authorization header (raw API key)
|
|
526
|
+
* + X-Tenant-ID header. No cookies.
|
|
510
527
|
*/
|
|
511
528
|
declare class ApiClient {
|
|
512
529
|
private readonly baseUrl;
|
|
@@ -515,7 +532,22 @@ declare class ApiClient {
|
|
|
515
532
|
private readonly baseDelay;
|
|
516
533
|
constructor(configuration: Configuration);
|
|
517
534
|
/**
|
|
518
|
-
* Get supported countries and their document types
|
|
535
|
+
* Get supported countries and their document types.
|
|
536
|
+
*
|
|
537
|
+
* Backend has no dedicated /supported-countries endpoint — the
|
|
538
|
+
* countries catalog is bundled into the /document-types response
|
|
539
|
+
* alongside the per-type metadata. We fetch that bundled payload,
|
|
540
|
+
* then project it onto the SDK's `SupportedCountry` shape:
|
|
541
|
+
* - `id` ← `code` (ISO-3166 alpha-2)
|
|
542
|
+
* - `flagEmoji` derived from the ISO code
|
|
543
|
+
* - `documentTypes` filtered from the bundled types list by country
|
|
544
|
+
*
|
|
545
|
+
* Mirrors the iOS / Android pattern (SessionManager.fetchSupported-
|
|
546
|
+
* Countries on iOS, ApiService.getDocumentTypes on Android). The
|
|
547
|
+
* previous standalone /supported-countries call had been silently
|
|
548
|
+
* 404-ing since the Web SDK shipped — surfaced 2026-05-29 by
|
|
549
|
+
* Luckycat's integration, hot on the heels of the v1.7.1
|
|
550
|
+
* wire-format pass.
|
|
519
551
|
*/
|
|
520
552
|
getSupportedCountries(): Promise<SupportedCountry[]>;
|
|
521
553
|
/**
|
|
@@ -529,30 +561,50 @@ declare class ApiClient {
|
|
|
529
561
|
/**
|
|
530
562
|
* Upload document image.
|
|
531
563
|
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
564
|
+
* Both sides now use the same JSON wire format as the Android/iOS SDKs.
|
|
565
|
+
* Front sends documentType + optional country; back sends optional
|
|
566
|
+
* decodedBarcodePayload (Phase 4 fast-path — when the browser's
|
|
567
|
+
* BarcodeDetector decoded the PDF417 on-device, the AAMVA payload
|
|
568
|
+
* travels here so the server can skip image-based barcode decoding).
|
|
569
|
+
*
|
|
570
|
+
* The previous front-side multipart path returned HTTP 400 on every
|
|
571
|
+
* request because the server's /document handler only parses
|
|
572
|
+
* application/json — same trap iOS hit and fixed in v1.6.0.
|
|
540
573
|
*/
|
|
541
|
-
uploadDocument(verificationId: string, imageData: Blob, side: 'front' | 'back', documentType: DocumentType, decodedBarcodePayload?: string): Promise<DocumentUploadResponse>;
|
|
574
|
+
uploadDocument(verificationId: string, imageData: Blob, side: 'front' | 'back', documentType: DocumentType, decodedBarcodePayload?: string, country?: string): Promise<DocumentUploadResponse>;
|
|
542
575
|
/**
|
|
543
|
-
* Upload selfie image
|
|
576
|
+
* Upload selfie image.
|
|
577
|
+
*
|
|
578
|
+
* JSON with imageBase64 — matches the server's UploadSelfieRequest
|
|
579
|
+
* { ImageBase64 string } and the Android/iOS wire format. Previous
|
|
580
|
+
* multipart path returned HTTP 400 since the SDK shipped.
|
|
544
581
|
*/
|
|
545
582
|
uploadSelfie(verificationId: string, imageData: Blob): Promise<SelfieUploadResponse>;
|
|
546
583
|
/**
|
|
547
|
-
* Create liveness session
|
|
584
|
+
* Create liveness session.
|
|
585
|
+
*
|
|
586
|
+
* Decodes the wire DTO (matches backend's `models.LivenessSession`
|
|
587
|
+
* JSON), then projects it onto the domain `LivenessSession` the UI
|
|
588
|
+
* expects. Backend doesn't send `sessionId`/`expiresAt`, and per-
|
|
589
|
+
* challenge `id`/`instruction`/`order` aren't on the wire — domain
|
|
590
|
+
* values are synthesized client-side. Mirrors the iOS v1.7.0 pattern
|
|
591
|
+
* at koraidv-ios/.../SessionManager.swift::createLivenessSession.
|
|
592
|
+
*
|
|
593
|
+
* Without this DTO/domain split, the SDK accessed undefined fields
|
|
594
|
+
* on the response and produced a session with sessionId=undefined,
|
|
595
|
+
* which then failed every downstream challenge submission.
|
|
548
596
|
*/
|
|
549
597
|
createLivenessSession(verificationId: string): Promise<LivenessSession>;
|
|
550
598
|
/**
|
|
551
|
-
* Submit liveness challenge
|
|
599
|
+
* Submit liveness challenge result.
|
|
600
|
+
*
|
|
601
|
+
* JSON with challengeType + imageBase64 — matches the server's
|
|
602
|
+
* SubmitLivenessChallengeRequest and the Android/iOS wire format.
|
|
603
|
+
* Previous multipart + snake_case path was double-broken.
|
|
552
604
|
*/
|
|
553
605
|
submitLivenessChallenge(verificationId: string, challenge: LivenessChallenge, imageData: Blob): Promise<LivenessChallengeResponse>;
|
|
554
606
|
/**
|
|
555
|
-
* Check document quality before uploading
|
|
607
|
+
* Check document quality before uploading.
|
|
556
608
|
*/
|
|
557
609
|
checkDocumentQuality(imageData: Blob, documentType: string): Promise<DocumentQualityResponse>;
|
|
558
610
|
/**
|
|
@@ -583,9 +635,13 @@ declare class ApiClient {
|
|
|
583
635
|
private shouldRetryNetworkError;
|
|
584
636
|
private calculateDelay;
|
|
585
637
|
private sleep;
|
|
586
|
-
private blobToBase64;
|
|
587
638
|
/**
|
|
588
|
-
* Transform snake_case response to camelCase
|
|
639
|
+
* Transform snake_case response to camelCase.
|
|
640
|
+
*
|
|
641
|
+
* Some backend endpoints still return snake_case keys; the
|
|
642
|
+
* domain types in ApiModels.ts are camelCase. This walker turns
|
|
643
|
+
* `external_id` → `externalId` etc. recursively. Safe no-op for
|
|
644
|
+
* already-camelCase responses.
|
|
589
645
|
*/
|
|
590
646
|
private transformResponse;
|
|
591
647
|
}
|
package/dist/index.js
CHANGED
|
@@ -538,10 +538,31 @@ var ApiClient = class {
|
|
|
538
538
|
this.baseUrl = environmentUrls[configuration.environment];
|
|
539
539
|
}
|
|
540
540
|
/**
|
|
541
|
-
* Get supported countries and their document types
|
|
541
|
+
* Get supported countries and their document types.
|
|
542
|
+
*
|
|
543
|
+
* Backend has no dedicated /supported-countries endpoint — the
|
|
544
|
+
* countries catalog is bundled into the /document-types response
|
|
545
|
+
* alongside the per-type metadata. We fetch that bundled payload,
|
|
546
|
+
* then project it onto the SDK's `SupportedCountry` shape:
|
|
547
|
+
* - `id` ← `code` (ISO-3166 alpha-2)
|
|
548
|
+
* - `flagEmoji` derived from the ISO code
|
|
549
|
+
* - `documentTypes` filtered from the bundled types list by country
|
|
550
|
+
*
|
|
551
|
+
* Mirrors the iOS / Android pattern (SessionManager.fetchSupported-
|
|
552
|
+
* Countries on iOS, ApiService.getDocumentTypes on Android). The
|
|
553
|
+
* previous standalone /supported-countries call had been silently
|
|
554
|
+
* 404-ing since the Web SDK shipped — surfaced 2026-05-29 by
|
|
555
|
+
* Luckycat's integration, hot on the heels of the v1.7.1
|
|
556
|
+
* wire-format pass.
|
|
542
557
|
*/
|
|
543
558
|
async getSupportedCountries() {
|
|
544
|
-
|
|
559
|
+
const response = await this.request("/document-types");
|
|
560
|
+
return response.countries.map((c) => ({
|
|
561
|
+
id: c.code,
|
|
562
|
+
name: c.name,
|
|
563
|
+
flagEmoji: countryCodeToFlagEmoji(c.code),
|
|
564
|
+
documentTypes: response.documentTypes.filter((dt) => dt.country === c.code).map((dt) => dt.type)
|
|
565
|
+
})).filter((country) => country.documentTypes.length > 0);
|
|
545
566
|
}
|
|
546
567
|
/**
|
|
547
568
|
* Create a new verification
|
|
@@ -550,7 +571,7 @@ var ApiClient = class {
|
|
|
550
571
|
return this.request("/verifications", {
|
|
551
572
|
method: "POST",
|
|
552
573
|
body: JSON.stringify({
|
|
553
|
-
|
|
574
|
+
externalId: request.externalId,
|
|
554
575
|
tier: request.tier
|
|
555
576
|
})
|
|
556
577
|
});
|
|
@@ -564,23 +585,23 @@ var ApiClient = class {
|
|
|
564
585
|
/**
|
|
565
586
|
* Upload document image.
|
|
566
587
|
*
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
588
|
+
* Both sides now use the same JSON wire format as the Android/iOS SDKs.
|
|
589
|
+
* Front sends documentType + optional country; back sends optional
|
|
590
|
+
* decodedBarcodePayload (Phase 4 fast-path — when the browser's
|
|
591
|
+
* BarcodeDetector decoded the PDF417 on-device, the AAMVA payload
|
|
592
|
+
* travels here so the server can skip image-based barcode decoding).
|
|
593
|
+
*
|
|
594
|
+
* The previous front-side multipart path returned HTTP 400 on every
|
|
595
|
+
* request because the server's /document handler only parses
|
|
596
|
+
* application/json — same trap iOS hit and fixed in v1.6.0.
|
|
575
597
|
*/
|
|
576
|
-
async uploadDocument(verificationId, imageData, side, documentType, decodedBarcodePayload) {
|
|
598
|
+
async uploadDocument(verificationId, imageData, side, documentType, decodedBarcodePayload, country) {
|
|
599
|
+
const imageBase64 = await blobToBase64(imageData);
|
|
577
600
|
if (side === "back") {
|
|
578
|
-
const imageBase64 = await blobToBase64(imageData);
|
|
579
601
|
return this.request(
|
|
580
602
|
`/verifications/${verificationId}/document/back`,
|
|
581
603
|
{
|
|
582
604
|
method: "POST",
|
|
583
|
-
headers: { "Content-Type": "application/json" },
|
|
584
605
|
body: JSON.stringify({
|
|
585
606
|
imageBase64,
|
|
586
607
|
decodedBarcodePayload: decodedBarcodePayload ?? null
|
|
@@ -588,80 +609,97 @@ var ApiClient = class {
|
|
|
588
609
|
}
|
|
589
610
|
);
|
|
590
611
|
}
|
|
591
|
-
const formData = new FormData();
|
|
592
|
-
formData.append("image", imageData, "document.jpg");
|
|
593
|
-
formData.append("document_type", documentType);
|
|
594
|
-
formData.append("side", side);
|
|
595
612
|
return this.request(
|
|
596
613
|
`/verifications/${verificationId}/document`,
|
|
597
614
|
{
|
|
598
615
|
method: "POST",
|
|
599
|
-
body:
|
|
600
|
-
|
|
601
|
-
|
|
616
|
+
body: JSON.stringify({
|
|
617
|
+
documentType,
|
|
618
|
+
imageBase64,
|
|
619
|
+
country: country ?? null
|
|
620
|
+
})
|
|
602
621
|
}
|
|
603
622
|
);
|
|
604
623
|
}
|
|
605
624
|
/**
|
|
606
|
-
* Upload selfie image
|
|
625
|
+
* Upload selfie image.
|
|
626
|
+
*
|
|
627
|
+
* JSON with imageBase64 — matches the server's UploadSelfieRequest
|
|
628
|
+
* { ImageBase64 string } and the Android/iOS wire format. Previous
|
|
629
|
+
* multipart path returned HTTP 400 since the SDK shipped.
|
|
607
630
|
*/
|
|
608
631
|
async uploadSelfie(verificationId, imageData) {
|
|
609
|
-
const
|
|
610
|
-
formData.append("image", imageData, "selfie.jpg");
|
|
632
|
+
const imageBase64 = await blobToBase64(imageData);
|
|
611
633
|
return this.request(`/verifications/${verificationId}/selfie`, {
|
|
612
634
|
method: "POST",
|
|
613
|
-
body:
|
|
614
|
-
headers: {}
|
|
635
|
+
body: JSON.stringify({ imageBase64 })
|
|
615
636
|
});
|
|
616
637
|
}
|
|
617
638
|
/**
|
|
618
|
-
* Create liveness session
|
|
639
|
+
* Create liveness session.
|
|
640
|
+
*
|
|
641
|
+
* Decodes the wire DTO (matches backend's `models.LivenessSession`
|
|
642
|
+
* JSON), then projects it onto the domain `LivenessSession` the UI
|
|
643
|
+
* expects. Backend doesn't send `sessionId`/`expiresAt`, and per-
|
|
644
|
+
* challenge `id`/`instruction`/`order` aren't on the wire — domain
|
|
645
|
+
* values are synthesized client-side. Mirrors the iOS v1.7.0 pattern
|
|
646
|
+
* at koraidv-ios/.../SessionManager.swift::createLivenessSession.
|
|
647
|
+
*
|
|
648
|
+
* Without this DTO/domain split, the SDK accessed undefined fields
|
|
649
|
+
* on the response and produced a session with sessionId=undefined,
|
|
650
|
+
* which then failed every downstream challenge submission.
|
|
619
651
|
*/
|
|
620
652
|
async createLivenessSession(verificationId) {
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
653
|
+
const dto = await this.request(
|
|
654
|
+
`/verifications/${verificationId}/liveness/session`,
|
|
655
|
+
{ method: "POST" }
|
|
656
|
+
);
|
|
624
657
|
return {
|
|
625
|
-
sessionId:
|
|
626
|
-
challenges:
|
|
627
|
-
id:
|
|
658
|
+
sessionId: dto.id,
|
|
659
|
+
challenges: dto.challenges.map((c, index) => ({
|
|
660
|
+
id: `${dto.id}_${index}`,
|
|
628
661
|
type: c.type,
|
|
629
|
-
instruction: c.
|
|
630
|
-
order:
|
|
662
|
+
instruction: instructionForChallengeType(c.type),
|
|
663
|
+
order: index
|
|
631
664
|
})),
|
|
632
|
-
expiresAt
|
|
665
|
+
// Backend doesn't return expiresAt; enforce a local 5-minute
|
|
666
|
+
// timeout consistent with the Android + iOS peers.
|
|
667
|
+
expiresAt: new Date(Date.now() + 5 * 60 * 1e3)
|
|
633
668
|
};
|
|
634
669
|
}
|
|
635
670
|
/**
|
|
636
|
-
* Submit liveness challenge
|
|
671
|
+
* Submit liveness challenge result.
|
|
672
|
+
*
|
|
673
|
+
* JSON with challengeType + imageBase64 — matches the server's
|
|
674
|
+
* SubmitLivenessChallengeRequest and the Android/iOS wire format.
|
|
675
|
+
* Previous multipart + snake_case path was double-broken.
|
|
637
676
|
*/
|
|
638
677
|
async submitLivenessChallenge(verificationId, challenge, imageData) {
|
|
639
|
-
const
|
|
640
|
-
formData.append("image", imageData, "challenge.jpg");
|
|
641
|
-
formData.append("challenge_type", challenge.type);
|
|
642
|
-
formData.append("challenge_id", challenge.id);
|
|
678
|
+
const imageBase64 = await blobToBase64(imageData);
|
|
643
679
|
const response = await this.request(`/verifications/${verificationId}/liveness/challenge`, {
|
|
644
680
|
method: "POST",
|
|
645
|
-
body:
|
|
646
|
-
|
|
681
|
+
body: JSON.stringify({
|
|
682
|
+
challengeType: challenge.type,
|
|
683
|
+
imageBase64
|
|
684
|
+
})
|
|
647
685
|
});
|
|
648
686
|
return {
|
|
649
|
-
success: response.success,
|
|
650
|
-
challengePassed: response.
|
|
651
|
-
confidence: response.confidence,
|
|
652
|
-
remainingChallenges: response.
|
|
687
|
+
success: response.success ?? true,
|
|
688
|
+
challengePassed: response.challengePassed ?? response.completed ?? false,
|
|
689
|
+
confidence: response.confidence ?? response.score ?? 0,
|
|
690
|
+
remainingChallenges: response.remainingChallenges ?? 0
|
|
653
691
|
};
|
|
654
692
|
}
|
|
655
693
|
/**
|
|
656
|
-
* Check document quality before uploading
|
|
694
|
+
* Check document quality before uploading.
|
|
657
695
|
*/
|
|
658
696
|
async checkDocumentQuality(imageData, documentType) {
|
|
659
|
-
const
|
|
697
|
+
const documentFrontBase64 = await blobToBase64(imageData);
|
|
660
698
|
return this.request("/kyc/document-quality", {
|
|
661
699
|
method: "POST",
|
|
662
700
|
body: JSON.stringify({
|
|
663
|
-
|
|
664
|
-
|
|
701
|
+
documentFrontBase64,
|
|
702
|
+
documentType
|
|
665
703
|
})
|
|
666
704
|
});
|
|
667
705
|
}
|
|
@@ -710,7 +748,7 @@ var ApiClient = class {
|
|
|
710
748
|
}
|
|
711
749
|
headers.set("X-Tenant-ID", this.configuration.tenantId);
|
|
712
750
|
headers.set("Accept", "application/json");
|
|
713
|
-
if (!
|
|
751
|
+
if (!headers.has("Content-Type")) {
|
|
714
752
|
headers.set("Content-Type", "application/json");
|
|
715
753
|
}
|
|
716
754
|
const requestOptions = {
|
|
@@ -781,20 +819,13 @@ var ApiClient = class {
|
|
|
781
819
|
sleep(ms) {
|
|
782
820
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
783
821
|
}
|
|
784
|
-
blobToBase64(blob) {
|
|
785
|
-
return new Promise((resolve, reject) => {
|
|
786
|
-
const reader = new FileReader();
|
|
787
|
-
reader.onloadend = () => {
|
|
788
|
-
const result = reader.result;
|
|
789
|
-
const base64 = result.split(",")[1] || result;
|
|
790
|
-
resolve(base64);
|
|
791
|
-
};
|
|
792
|
-
reader.onerror = reject;
|
|
793
|
-
reader.readAsDataURL(blob);
|
|
794
|
-
});
|
|
795
|
-
}
|
|
796
822
|
/**
|
|
797
|
-
* Transform snake_case response to camelCase
|
|
823
|
+
* Transform snake_case response to camelCase.
|
|
824
|
+
*
|
|
825
|
+
* Some backend endpoints still return snake_case keys; the
|
|
826
|
+
* domain types in ApiModels.ts are camelCase. This walker turns
|
|
827
|
+
* `external_id` → `externalId` etc. recursively. Safe no-op for
|
|
828
|
+
* already-camelCase responses.
|
|
798
829
|
*/
|
|
799
830
|
transformResponse(data) {
|
|
800
831
|
if (Array.isArray(data)) {
|
|
@@ -811,6 +842,33 @@ var ApiClient = class {
|
|
|
811
842
|
return data;
|
|
812
843
|
}
|
|
813
844
|
};
|
|
845
|
+
function instructionForChallengeType(type) {
|
|
846
|
+
switch (type) {
|
|
847
|
+
case "blink":
|
|
848
|
+
return "Blink your eyes slowly";
|
|
849
|
+
case "smile":
|
|
850
|
+
return "Smile naturally";
|
|
851
|
+
case "turn_left":
|
|
852
|
+
return "Slowly turn your head to the left";
|
|
853
|
+
case "turn_right":
|
|
854
|
+
return "Slowly turn your head to the right";
|
|
855
|
+
case "nod_up":
|
|
856
|
+
return "Slowly tilt your head up";
|
|
857
|
+
case "nod_down":
|
|
858
|
+
return "Slowly tilt your head down";
|
|
859
|
+
default:
|
|
860
|
+
return "Follow the on-screen prompt";
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
function countryCodeToFlagEmoji(code) {
|
|
864
|
+
if (!code || code.length !== 2) return "";
|
|
865
|
+
const upper = code.toUpperCase();
|
|
866
|
+
const a = upper.charCodeAt(0);
|
|
867
|
+
const b = upper.charCodeAt(1);
|
|
868
|
+
if (a < 65 || a > 90 || b < 65 || b > 90) return "";
|
|
869
|
+
const offset = 127462 - 65;
|
|
870
|
+
return String.fromCodePoint(a + offset, b + offset);
|
|
871
|
+
}
|
|
814
872
|
|
|
815
873
|
// src/KoraIDV.ts
|
|
816
874
|
var KoraIDV = class {
|
package/dist/index.mjs
CHANGED
|
@@ -494,10 +494,31 @@ var ApiClient = class {
|
|
|
494
494
|
this.baseUrl = environmentUrls[configuration.environment];
|
|
495
495
|
}
|
|
496
496
|
/**
|
|
497
|
-
* Get supported countries and their document types
|
|
497
|
+
* Get supported countries and their document types.
|
|
498
|
+
*
|
|
499
|
+
* Backend has no dedicated /supported-countries endpoint — the
|
|
500
|
+
* countries catalog is bundled into the /document-types response
|
|
501
|
+
* alongside the per-type metadata. We fetch that bundled payload,
|
|
502
|
+
* then project it onto the SDK's `SupportedCountry` shape:
|
|
503
|
+
* - `id` ← `code` (ISO-3166 alpha-2)
|
|
504
|
+
* - `flagEmoji` derived from the ISO code
|
|
505
|
+
* - `documentTypes` filtered from the bundled types list by country
|
|
506
|
+
*
|
|
507
|
+
* Mirrors the iOS / Android pattern (SessionManager.fetchSupported-
|
|
508
|
+
* Countries on iOS, ApiService.getDocumentTypes on Android). The
|
|
509
|
+
* previous standalone /supported-countries call had been silently
|
|
510
|
+
* 404-ing since the Web SDK shipped — surfaced 2026-05-29 by
|
|
511
|
+
* Luckycat's integration, hot on the heels of the v1.7.1
|
|
512
|
+
* wire-format pass.
|
|
498
513
|
*/
|
|
499
514
|
async getSupportedCountries() {
|
|
500
|
-
|
|
515
|
+
const response = await this.request("/document-types");
|
|
516
|
+
return response.countries.map((c) => ({
|
|
517
|
+
id: c.code,
|
|
518
|
+
name: c.name,
|
|
519
|
+
flagEmoji: countryCodeToFlagEmoji(c.code),
|
|
520
|
+
documentTypes: response.documentTypes.filter((dt) => dt.country === c.code).map((dt) => dt.type)
|
|
521
|
+
})).filter((country) => country.documentTypes.length > 0);
|
|
501
522
|
}
|
|
502
523
|
/**
|
|
503
524
|
* Create a new verification
|
|
@@ -506,7 +527,7 @@ var ApiClient = class {
|
|
|
506
527
|
return this.request("/verifications", {
|
|
507
528
|
method: "POST",
|
|
508
529
|
body: JSON.stringify({
|
|
509
|
-
|
|
530
|
+
externalId: request.externalId,
|
|
510
531
|
tier: request.tier
|
|
511
532
|
})
|
|
512
533
|
});
|
|
@@ -520,23 +541,23 @@ var ApiClient = class {
|
|
|
520
541
|
/**
|
|
521
542
|
* Upload document image.
|
|
522
543
|
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
544
|
+
* Both sides now use the same JSON wire format as the Android/iOS SDKs.
|
|
545
|
+
* Front sends documentType + optional country; back sends optional
|
|
546
|
+
* decodedBarcodePayload (Phase 4 fast-path — when the browser's
|
|
547
|
+
* BarcodeDetector decoded the PDF417 on-device, the AAMVA payload
|
|
548
|
+
* travels here so the server can skip image-based barcode decoding).
|
|
549
|
+
*
|
|
550
|
+
* The previous front-side multipart path returned HTTP 400 on every
|
|
551
|
+
* request because the server's /document handler only parses
|
|
552
|
+
* application/json — same trap iOS hit and fixed in v1.6.0.
|
|
531
553
|
*/
|
|
532
|
-
async uploadDocument(verificationId, imageData, side, documentType, decodedBarcodePayload) {
|
|
554
|
+
async uploadDocument(verificationId, imageData, side, documentType, decodedBarcodePayload, country) {
|
|
555
|
+
const imageBase64 = await blobToBase64(imageData);
|
|
533
556
|
if (side === "back") {
|
|
534
|
-
const imageBase64 = await blobToBase64(imageData);
|
|
535
557
|
return this.request(
|
|
536
558
|
`/verifications/${verificationId}/document/back`,
|
|
537
559
|
{
|
|
538
560
|
method: "POST",
|
|
539
|
-
headers: { "Content-Type": "application/json" },
|
|
540
561
|
body: JSON.stringify({
|
|
541
562
|
imageBase64,
|
|
542
563
|
decodedBarcodePayload: decodedBarcodePayload ?? null
|
|
@@ -544,80 +565,97 @@ var ApiClient = class {
|
|
|
544
565
|
}
|
|
545
566
|
);
|
|
546
567
|
}
|
|
547
|
-
const formData = new FormData();
|
|
548
|
-
formData.append("image", imageData, "document.jpg");
|
|
549
|
-
formData.append("document_type", documentType);
|
|
550
|
-
formData.append("side", side);
|
|
551
568
|
return this.request(
|
|
552
569
|
`/verifications/${verificationId}/document`,
|
|
553
570
|
{
|
|
554
571
|
method: "POST",
|
|
555
|
-
body:
|
|
556
|
-
|
|
557
|
-
|
|
572
|
+
body: JSON.stringify({
|
|
573
|
+
documentType,
|
|
574
|
+
imageBase64,
|
|
575
|
+
country: country ?? null
|
|
576
|
+
})
|
|
558
577
|
}
|
|
559
578
|
);
|
|
560
579
|
}
|
|
561
580
|
/**
|
|
562
|
-
* Upload selfie image
|
|
581
|
+
* Upload selfie image.
|
|
582
|
+
*
|
|
583
|
+
* JSON with imageBase64 — matches the server's UploadSelfieRequest
|
|
584
|
+
* { ImageBase64 string } and the Android/iOS wire format. Previous
|
|
585
|
+
* multipart path returned HTTP 400 since the SDK shipped.
|
|
563
586
|
*/
|
|
564
587
|
async uploadSelfie(verificationId, imageData) {
|
|
565
|
-
const
|
|
566
|
-
formData.append("image", imageData, "selfie.jpg");
|
|
588
|
+
const imageBase64 = await blobToBase64(imageData);
|
|
567
589
|
return this.request(`/verifications/${verificationId}/selfie`, {
|
|
568
590
|
method: "POST",
|
|
569
|
-
body:
|
|
570
|
-
headers: {}
|
|
591
|
+
body: JSON.stringify({ imageBase64 })
|
|
571
592
|
});
|
|
572
593
|
}
|
|
573
594
|
/**
|
|
574
|
-
* Create liveness session
|
|
595
|
+
* Create liveness session.
|
|
596
|
+
*
|
|
597
|
+
* Decodes the wire DTO (matches backend's `models.LivenessSession`
|
|
598
|
+
* JSON), then projects it onto the domain `LivenessSession` the UI
|
|
599
|
+
* expects. Backend doesn't send `sessionId`/`expiresAt`, and per-
|
|
600
|
+
* challenge `id`/`instruction`/`order` aren't on the wire — domain
|
|
601
|
+
* values are synthesized client-side. Mirrors the iOS v1.7.0 pattern
|
|
602
|
+
* at koraidv-ios/.../SessionManager.swift::createLivenessSession.
|
|
603
|
+
*
|
|
604
|
+
* Without this DTO/domain split, the SDK accessed undefined fields
|
|
605
|
+
* on the response and produced a session with sessionId=undefined,
|
|
606
|
+
* which then failed every downstream challenge submission.
|
|
575
607
|
*/
|
|
576
608
|
async createLivenessSession(verificationId) {
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
609
|
+
const dto = await this.request(
|
|
610
|
+
`/verifications/${verificationId}/liveness/session`,
|
|
611
|
+
{ method: "POST" }
|
|
612
|
+
);
|
|
580
613
|
return {
|
|
581
|
-
sessionId:
|
|
582
|
-
challenges:
|
|
583
|
-
id:
|
|
614
|
+
sessionId: dto.id,
|
|
615
|
+
challenges: dto.challenges.map((c, index) => ({
|
|
616
|
+
id: `${dto.id}_${index}`,
|
|
584
617
|
type: c.type,
|
|
585
|
-
instruction: c.
|
|
586
|
-
order:
|
|
618
|
+
instruction: instructionForChallengeType(c.type),
|
|
619
|
+
order: index
|
|
587
620
|
})),
|
|
588
|
-
expiresAt
|
|
621
|
+
// Backend doesn't return expiresAt; enforce a local 5-minute
|
|
622
|
+
// timeout consistent with the Android + iOS peers.
|
|
623
|
+
expiresAt: new Date(Date.now() + 5 * 60 * 1e3)
|
|
589
624
|
};
|
|
590
625
|
}
|
|
591
626
|
/**
|
|
592
|
-
* Submit liveness challenge
|
|
627
|
+
* Submit liveness challenge result.
|
|
628
|
+
*
|
|
629
|
+
* JSON with challengeType + imageBase64 — matches the server's
|
|
630
|
+
* SubmitLivenessChallengeRequest and the Android/iOS wire format.
|
|
631
|
+
* Previous multipart + snake_case path was double-broken.
|
|
593
632
|
*/
|
|
594
633
|
async submitLivenessChallenge(verificationId, challenge, imageData) {
|
|
595
|
-
const
|
|
596
|
-
formData.append("image", imageData, "challenge.jpg");
|
|
597
|
-
formData.append("challenge_type", challenge.type);
|
|
598
|
-
formData.append("challenge_id", challenge.id);
|
|
634
|
+
const imageBase64 = await blobToBase64(imageData);
|
|
599
635
|
const response = await this.request(`/verifications/${verificationId}/liveness/challenge`, {
|
|
600
636
|
method: "POST",
|
|
601
|
-
body:
|
|
602
|
-
|
|
637
|
+
body: JSON.stringify({
|
|
638
|
+
challengeType: challenge.type,
|
|
639
|
+
imageBase64
|
|
640
|
+
})
|
|
603
641
|
});
|
|
604
642
|
return {
|
|
605
|
-
success: response.success,
|
|
606
|
-
challengePassed: response.
|
|
607
|
-
confidence: response.confidence,
|
|
608
|
-
remainingChallenges: response.
|
|
643
|
+
success: response.success ?? true,
|
|
644
|
+
challengePassed: response.challengePassed ?? response.completed ?? false,
|
|
645
|
+
confidence: response.confidence ?? response.score ?? 0,
|
|
646
|
+
remainingChallenges: response.remainingChallenges ?? 0
|
|
609
647
|
};
|
|
610
648
|
}
|
|
611
649
|
/**
|
|
612
|
-
* Check document quality before uploading
|
|
650
|
+
* Check document quality before uploading.
|
|
613
651
|
*/
|
|
614
652
|
async checkDocumentQuality(imageData, documentType) {
|
|
615
|
-
const
|
|
653
|
+
const documentFrontBase64 = await blobToBase64(imageData);
|
|
616
654
|
return this.request("/kyc/document-quality", {
|
|
617
655
|
method: "POST",
|
|
618
656
|
body: JSON.stringify({
|
|
619
|
-
|
|
620
|
-
|
|
657
|
+
documentFrontBase64,
|
|
658
|
+
documentType
|
|
621
659
|
})
|
|
622
660
|
});
|
|
623
661
|
}
|
|
@@ -666,7 +704,7 @@ var ApiClient = class {
|
|
|
666
704
|
}
|
|
667
705
|
headers.set("X-Tenant-ID", this.configuration.tenantId);
|
|
668
706
|
headers.set("Accept", "application/json");
|
|
669
|
-
if (!
|
|
707
|
+
if (!headers.has("Content-Type")) {
|
|
670
708
|
headers.set("Content-Type", "application/json");
|
|
671
709
|
}
|
|
672
710
|
const requestOptions = {
|
|
@@ -737,20 +775,13 @@ var ApiClient = class {
|
|
|
737
775
|
sleep(ms) {
|
|
738
776
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
739
777
|
}
|
|
740
|
-
blobToBase64(blob) {
|
|
741
|
-
return new Promise((resolve, reject) => {
|
|
742
|
-
const reader = new FileReader();
|
|
743
|
-
reader.onloadend = () => {
|
|
744
|
-
const result = reader.result;
|
|
745
|
-
const base64 = result.split(",")[1] || result;
|
|
746
|
-
resolve(base64);
|
|
747
|
-
};
|
|
748
|
-
reader.onerror = reject;
|
|
749
|
-
reader.readAsDataURL(blob);
|
|
750
|
-
});
|
|
751
|
-
}
|
|
752
778
|
/**
|
|
753
|
-
* Transform snake_case response to camelCase
|
|
779
|
+
* Transform snake_case response to camelCase.
|
|
780
|
+
*
|
|
781
|
+
* Some backend endpoints still return snake_case keys; the
|
|
782
|
+
* domain types in ApiModels.ts are camelCase. This walker turns
|
|
783
|
+
* `external_id` → `externalId` etc. recursively. Safe no-op for
|
|
784
|
+
* already-camelCase responses.
|
|
754
785
|
*/
|
|
755
786
|
transformResponse(data) {
|
|
756
787
|
if (Array.isArray(data)) {
|
|
@@ -767,6 +798,33 @@ var ApiClient = class {
|
|
|
767
798
|
return data;
|
|
768
799
|
}
|
|
769
800
|
};
|
|
801
|
+
function instructionForChallengeType(type) {
|
|
802
|
+
switch (type) {
|
|
803
|
+
case "blink":
|
|
804
|
+
return "Blink your eyes slowly";
|
|
805
|
+
case "smile":
|
|
806
|
+
return "Smile naturally";
|
|
807
|
+
case "turn_left":
|
|
808
|
+
return "Slowly turn your head to the left";
|
|
809
|
+
case "turn_right":
|
|
810
|
+
return "Slowly turn your head to the right";
|
|
811
|
+
case "nod_up":
|
|
812
|
+
return "Slowly tilt your head up";
|
|
813
|
+
case "nod_down":
|
|
814
|
+
return "Slowly tilt your head down";
|
|
815
|
+
default:
|
|
816
|
+
return "Follow the on-screen prompt";
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
function countryCodeToFlagEmoji(code) {
|
|
820
|
+
if (!code || code.length !== 2) return "";
|
|
821
|
+
const upper = code.toUpperCase();
|
|
822
|
+
const a = upper.charCodeAt(0);
|
|
823
|
+
const b = upper.charCodeAt(1);
|
|
824
|
+
if (a < 65 || a > 90 || b < 65 || b > 90) return "";
|
|
825
|
+
const offset = 127462 - 65;
|
|
826
|
+
return String.fromCodePoint(a + offset, b + offset);
|
|
827
|
+
}
|
|
770
828
|
|
|
771
829
|
// src/KoraIDV.ts
|
|
772
830
|
var KoraIDV = class {
|