@koraidv/core 1.5.5 → 1.7.1
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 +58 -17
- package/dist/index.d.ts +58 -17
- package/dist/index.js +92 -64
- package/dist/index.mjs +92 -64
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -66,7 +66,7 @@ declare function getDocumentTypeInfo(type: DocumentType): DocumentTypeInfo;
|
|
|
66
66
|
* SDK Configuration
|
|
67
67
|
*/
|
|
68
68
|
interface Configuration {
|
|
69
|
-
/** API key
|
|
69
|
+
/** API key. Sandbox keys start with `sk_sandbox_`, production with `sk_live_`. */
|
|
70
70
|
apiKey: string;
|
|
71
71
|
/** Tenant ID (UUID) */
|
|
72
72
|
tenantId: string;
|
|
@@ -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;
|
|
@@ -529,30 +546,50 @@ declare class ApiClient {
|
|
|
529
546
|
/**
|
|
530
547
|
* Upload document image.
|
|
531
548
|
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
549
|
+
* Both sides now use the same JSON wire format as the Android/iOS SDKs.
|
|
550
|
+
* Front sends documentType + optional country; back sends optional
|
|
551
|
+
* decodedBarcodePayload (Phase 4 fast-path — when the browser's
|
|
552
|
+
* BarcodeDetector decoded the PDF417 on-device, the AAMVA payload
|
|
553
|
+
* travels here so the server can skip image-based barcode decoding).
|
|
554
|
+
*
|
|
555
|
+
* The previous front-side multipart path returned HTTP 400 on every
|
|
556
|
+
* request because the server's /document handler only parses
|
|
557
|
+
* application/json — same trap iOS hit and fixed in v1.6.0.
|
|
540
558
|
*/
|
|
541
|
-
uploadDocument(verificationId: string, imageData: Blob, side: 'front' | 'back', documentType: DocumentType, decodedBarcodePayload?: string): Promise<DocumentUploadResponse>;
|
|
559
|
+
uploadDocument(verificationId: string, imageData: Blob, side: 'front' | 'back', documentType: DocumentType, decodedBarcodePayload?: string, country?: string): Promise<DocumentUploadResponse>;
|
|
542
560
|
/**
|
|
543
|
-
* Upload selfie image
|
|
561
|
+
* Upload selfie image.
|
|
562
|
+
*
|
|
563
|
+
* JSON with imageBase64 — matches the server's UploadSelfieRequest
|
|
564
|
+
* { ImageBase64 string } and the Android/iOS wire format. Previous
|
|
565
|
+
* multipart path returned HTTP 400 since the SDK shipped.
|
|
544
566
|
*/
|
|
545
567
|
uploadSelfie(verificationId: string, imageData: Blob): Promise<SelfieUploadResponse>;
|
|
546
568
|
/**
|
|
547
|
-
* Create liveness session
|
|
569
|
+
* Create liveness session.
|
|
570
|
+
*
|
|
571
|
+
* Decodes the wire DTO (matches backend's `models.LivenessSession`
|
|
572
|
+
* JSON), then projects it onto the domain `LivenessSession` the UI
|
|
573
|
+
* expects. Backend doesn't send `sessionId`/`expiresAt`, and per-
|
|
574
|
+
* challenge `id`/`instruction`/`order` aren't on the wire — domain
|
|
575
|
+
* values are synthesized client-side. Mirrors the iOS v1.7.0 pattern
|
|
576
|
+
* at koraidv-ios/.../SessionManager.swift::createLivenessSession.
|
|
577
|
+
*
|
|
578
|
+
* Without this DTO/domain split, the SDK accessed undefined fields
|
|
579
|
+
* on the response and produced a session with sessionId=undefined,
|
|
580
|
+
* which then failed every downstream challenge submission.
|
|
548
581
|
*/
|
|
549
582
|
createLivenessSession(verificationId: string): Promise<LivenessSession>;
|
|
550
583
|
/**
|
|
551
|
-
* Submit liveness challenge
|
|
584
|
+
* Submit liveness challenge result.
|
|
585
|
+
*
|
|
586
|
+
* JSON with challengeType + imageBase64 — matches the server's
|
|
587
|
+
* SubmitLivenessChallengeRequest and the Android/iOS wire format.
|
|
588
|
+
* Previous multipart + snake_case path was double-broken.
|
|
552
589
|
*/
|
|
553
590
|
submitLivenessChallenge(verificationId: string, challenge: LivenessChallenge, imageData: Blob): Promise<LivenessChallengeResponse>;
|
|
554
591
|
/**
|
|
555
|
-
* Check document quality before uploading
|
|
592
|
+
* Check document quality before uploading.
|
|
556
593
|
*/
|
|
557
594
|
checkDocumentQuality(imageData: Blob, documentType: string): Promise<DocumentQualityResponse>;
|
|
558
595
|
/**
|
|
@@ -583,9 +620,13 @@ declare class ApiClient {
|
|
|
583
620
|
private shouldRetryNetworkError;
|
|
584
621
|
private calculateDelay;
|
|
585
622
|
private sleep;
|
|
586
|
-
private blobToBase64;
|
|
587
623
|
/**
|
|
588
|
-
* Transform snake_case response to camelCase
|
|
624
|
+
* Transform snake_case response to camelCase.
|
|
625
|
+
*
|
|
626
|
+
* Some backend endpoints still return snake_case keys; the
|
|
627
|
+
* domain types in ApiModels.ts are camelCase. This walker turns
|
|
628
|
+
* `external_id` → `externalId` etc. recursively. Safe no-op for
|
|
629
|
+
* already-camelCase responses.
|
|
589
630
|
*/
|
|
590
631
|
private transformResponse;
|
|
591
632
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ declare function getDocumentTypeInfo(type: DocumentType): DocumentTypeInfo;
|
|
|
66
66
|
* SDK Configuration
|
|
67
67
|
*/
|
|
68
68
|
interface Configuration {
|
|
69
|
-
/** API key
|
|
69
|
+
/** API key. Sandbox keys start with `sk_sandbox_`, production with `sk_live_`. */
|
|
70
70
|
apiKey: string;
|
|
71
71
|
/** Tenant ID (UUID) */
|
|
72
72
|
tenantId: string;
|
|
@@ -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;
|
|
@@ -529,30 +546,50 @@ declare class ApiClient {
|
|
|
529
546
|
/**
|
|
530
547
|
* Upload document image.
|
|
531
548
|
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
549
|
+
* Both sides now use the same JSON wire format as the Android/iOS SDKs.
|
|
550
|
+
* Front sends documentType + optional country; back sends optional
|
|
551
|
+
* decodedBarcodePayload (Phase 4 fast-path — when the browser's
|
|
552
|
+
* BarcodeDetector decoded the PDF417 on-device, the AAMVA payload
|
|
553
|
+
* travels here so the server can skip image-based barcode decoding).
|
|
554
|
+
*
|
|
555
|
+
* The previous front-side multipart path returned HTTP 400 on every
|
|
556
|
+
* request because the server's /document handler only parses
|
|
557
|
+
* application/json — same trap iOS hit and fixed in v1.6.0.
|
|
540
558
|
*/
|
|
541
|
-
uploadDocument(verificationId: string, imageData: Blob, side: 'front' | 'back', documentType: DocumentType, decodedBarcodePayload?: string): Promise<DocumentUploadResponse>;
|
|
559
|
+
uploadDocument(verificationId: string, imageData: Blob, side: 'front' | 'back', documentType: DocumentType, decodedBarcodePayload?: string, country?: string): Promise<DocumentUploadResponse>;
|
|
542
560
|
/**
|
|
543
|
-
* Upload selfie image
|
|
561
|
+
* Upload selfie image.
|
|
562
|
+
*
|
|
563
|
+
* JSON with imageBase64 — matches the server's UploadSelfieRequest
|
|
564
|
+
* { ImageBase64 string } and the Android/iOS wire format. Previous
|
|
565
|
+
* multipart path returned HTTP 400 since the SDK shipped.
|
|
544
566
|
*/
|
|
545
567
|
uploadSelfie(verificationId: string, imageData: Blob): Promise<SelfieUploadResponse>;
|
|
546
568
|
/**
|
|
547
|
-
* Create liveness session
|
|
569
|
+
* Create liveness session.
|
|
570
|
+
*
|
|
571
|
+
* Decodes the wire DTO (matches backend's `models.LivenessSession`
|
|
572
|
+
* JSON), then projects it onto the domain `LivenessSession` the UI
|
|
573
|
+
* expects. Backend doesn't send `sessionId`/`expiresAt`, and per-
|
|
574
|
+
* challenge `id`/`instruction`/`order` aren't on the wire — domain
|
|
575
|
+
* values are synthesized client-side. Mirrors the iOS v1.7.0 pattern
|
|
576
|
+
* at koraidv-ios/.../SessionManager.swift::createLivenessSession.
|
|
577
|
+
*
|
|
578
|
+
* Without this DTO/domain split, the SDK accessed undefined fields
|
|
579
|
+
* on the response and produced a session with sessionId=undefined,
|
|
580
|
+
* which then failed every downstream challenge submission.
|
|
548
581
|
*/
|
|
549
582
|
createLivenessSession(verificationId: string): Promise<LivenessSession>;
|
|
550
583
|
/**
|
|
551
|
-
* Submit liveness challenge
|
|
584
|
+
* Submit liveness challenge result.
|
|
585
|
+
*
|
|
586
|
+
* JSON with challengeType + imageBase64 — matches the server's
|
|
587
|
+
* SubmitLivenessChallengeRequest and the Android/iOS wire format.
|
|
588
|
+
* Previous multipart + snake_case path was double-broken.
|
|
552
589
|
*/
|
|
553
590
|
submitLivenessChallenge(verificationId: string, challenge: LivenessChallenge, imageData: Blob): Promise<LivenessChallengeResponse>;
|
|
554
591
|
/**
|
|
555
|
-
* Check document quality before uploading
|
|
592
|
+
* Check document quality before uploading.
|
|
556
593
|
*/
|
|
557
594
|
checkDocumentQuality(imageData: Blob, documentType: string): Promise<DocumentQualityResponse>;
|
|
558
595
|
/**
|
|
@@ -583,9 +620,13 @@ declare class ApiClient {
|
|
|
583
620
|
private shouldRetryNetworkError;
|
|
584
621
|
private calculateDelay;
|
|
585
622
|
private sleep;
|
|
586
|
-
private blobToBase64;
|
|
587
623
|
/**
|
|
588
|
-
* Transform snake_case response to camelCase
|
|
624
|
+
* Transform snake_case response to camelCase.
|
|
625
|
+
*
|
|
626
|
+
* Some backend endpoints still return snake_case keys; the
|
|
627
|
+
* domain types in ApiModels.ts are camelCase. This walker turns
|
|
628
|
+
* `external_id` → `externalId` etc. recursively. Safe no-op for
|
|
629
|
+
* already-camelCase responses.
|
|
589
630
|
*/
|
|
590
631
|
private transformResponse;
|
|
591
632
|
}
|
package/dist/index.js
CHANGED
|
@@ -550,7 +550,7 @@ var ApiClient = class {
|
|
|
550
550
|
return this.request("/verifications", {
|
|
551
551
|
method: "POST",
|
|
552
552
|
body: JSON.stringify({
|
|
553
|
-
|
|
553
|
+
externalId: request.externalId,
|
|
554
554
|
tier: request.tier
|
|
555
555
|
})
|
|
556
556
|
});
|
|
@@ -564,23 +564,23 @@ var ApiClient = class {
|
|
|
564
564
|
/**
|
|
565
565
|
* Upload document image.
|
|
566
566
|
*
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
567
|
+
* Both sides now use the same JSON wire format as the Android/iOS SDKs.
|
|
568
|
+
* Front sends documentType + optional country; back sends optional
|
|
569
|
+
* decodedBarcodePayload (Phase 4 fast-path — when the browser's
|
|
570
|
+
* BarcodeDetector decoded the PDF417 on-device, the AAMVA payload
|
|
571
|
+
* travels here so the server can skip image-based barcode decoding).
|
|
572
|
+
*
|
|
573
|
+
* The previous front-side multipart path returned HTTP 400 on every
|
|
574
|
+
* request because the server's /document handler only parses
|
|
575
|
+
* application/json — same trap iOS hit and fixed in v1.6.0.
|
|
575
576
|
*/
|
|
576
|
-
async uploadDocument(verificationId, imageData, side, documentType, decodedBarcodePayload) {
|
|
577
|
+
async uploadDocument(verificationId, imageData, side, documentType, decodedBarcodePayload, country) {
|
|
578
|
+
const imageBase64 = await blobToBase64(imageData);
|
|
577
579
|
if (side === "back") {
|
|
578
|
-
const imageBase64 = await blobToBase64(imageData);
|
|
579
580
|
return this.request(
|
|
580
581
|
`/verifications/${verificationId}/document/back`,
|
|
581
582
|
{
|
|
582
583
|
method: "POST",
|
|
583
|
-
headers: { "Content-Type": "application/json" },
|
|
584
584
|
body: JSON.stringify({
|
|
585
585
|
imageBase64,
|
|
586
586
|
decodedBarcodePayload: decodedBarcodePayload ?? null
|
|
@@ -588,80 +588,97 @@ var ApiClient = class {
|
|
|
588
588
|
}
|
|
589
589
|
);
|
|
590
590
|
}
|
|
591
|
-
const formData = new FormData();
|
|
592
|
-
formData.append("image", imageData, "document.jpg");
|
|
593
|
-
formData.append("document_type", documentType);
|
|
594
|
-
formData.append("side", side);
|
|
595
591
|
return this.request(
|
|
596
592
|
`/verifications/${verificationId}/document`,
|
|
597
593
|
{
|
|
598
594
|
method: "POST",
|
|
599
|
-
body:
|
|
600
|
-
|
|
601
|
-
|
|
595
|
+
body: JSON.stringify({
|
|
596
|
+
documentType,
|
|
597
|
+
imageBase64,
|
|
598
|
+
country: country ?? null
|
|
599
|
+
})
|
|
602
600
|
}
|
|
603
601
|
);
|
|
604
602
|
}
|
|
605
603
|
/**
|
|
606
|
-
* Upload selfie image
|
|
604
|
+
* Upload selfie image.
|
|
605
|
+
*
|
|
606
|
+
* JSON with imageBase64 — matches the server's UploadSelfieRequest
|
|
607
|
+
* { ImageBase64 string } and the Android/iOS wire format. Previous
|
|
608
|
+
* multipart path returned HTTP 400 since the SDK shipped.
|
|
607
609
|
*/
|
|
608
610
|
async uploadSelfie(verificationId, imageData) {
|
|
609
|
-
const
|
|
610
|
-
formData.append("image", imageData, "selfie.jpg");
|
|
611
|
+
const imageBase64 = await blobToBase64(imageData);
|
|
611
612
|
return this.request(`/verifications/${verificationId}/selfie`, {
|
|
612
613
|
method: "POST",
|
|
613
|
-
body:
|
|
614
|
-
headers: {}
|
|
614
|
+
body: JSON.stringify({ imageBase64 })
|
|
615
615
|
});
|
|
616
616
|
}
|
|
617
617
|
/**
|
|
618
|
-
* Create liveness session
|
|
618
|
+
* Create liveness session.
|
|
619
|
+
*
|
|
620
|
+
* Decodes the wire DTO (matches backend's `models.LivenessSession`
|
|
621
|
+
* JSON), then projects it onto the domain `LivenessSession` the UI
|
|
622
|
+
* expects. Backend doesn't send `sessionId`/`expiresAt`, and per-
|
|
623
|
+
* challenge `id`/`instruction`/`order` aren't on the wire — domain
|
|
624
|
+
* values are synthesized client-side. Mirrors the iOS v1.7.0 pattern
|
|
625
|
+
* at koraidv-ios/.../SessionManager.swift::createLivenessSession.
|
|
626
|
+
*
|
|
627
|
+
* Without this DTO/domain split, the SDK accessed undefined fields
|
|
628
|
+
* on the response and produced a session with sessionId=undefined,
|
|
629
|
+
* which then failed every downstream challenge submission.
|
|
619
630
|
*/
|
|
620
631
|
async createLivenessSession(verificationId) {
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
632
|
+
const dto = await this.request(
|
|
633
|
+
`/verifications/${verificationId}/liveness/session`,
|
|
634
|
+
{ method: "POST" }
|
|
635
|
+
);
|
|
624
636
|
return {
|
|
625
|
-
sessionId:
|
|
626
|
-
challenges:
|
|
627
|
-
id:
|
|
637
|
+
sessionId: dto.id,
|
|
638
|
+
challenges: dto.challenges.map((c, index) => ({
|
|
639
|
+
id: `${dto.id}_${index}`,
|
|
628
640
|
type: c.type,
|
|
629
|
-
instruction: c.
|
|
630
|
-
order:
|
|
641
|
+
instruction: instructionForChallengeType(c.type),
|
|
642
|
+
order: index
|
|
631
643
|
})),
|
|
632
|
-
expiresAt
|
|
644
|
+
// Backend doesn't return expiresAt; enforce a local 5-minute
|
|
645
|
+
// timeout consistent with the Android + iOS peers.
|
|
646
|
+
expiresAt: new Date(Date.now() + 5 * 60 * 1e3)
|
|
633
647
|
};
|
|
634
648
|
}
|
|
635
649
|
/**
|
|
636
|
-
* Submit liveness challenge
|
|
650
|
+
* Submit liveness challenge result.
|
|
651
|
+
*
|
|
652
|
+
* JSON with challengeType + imageBase64 — matches the server's
|
|
653
|
+
* SubmitLivenessChallengeRequest and the Android/iOS wire format.
|
|
654
|
+
* Previous multipart + snake_case path was double-broken.
|
|
637
655
|
*/
|
|
638
656
|
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);
|
|
657
|
+
const imageBase64 = await blobToBase64(imageData);
|
|
643
658
|
const response = await this.request(`/verifications/${verificationId}/liveness/challenge`, {
|
|
644
659
|
method: "POST",
|
|
645
|
-
body:
|
|
646
|
-
|
|
660
|
+
body: JSON.stringify({
|
|
661
|
+
challengeType: challenge.type,
|
|
662
|
+
imageBase64
|
|
663
|
+
})
|
|
647
664
|
});
|
|
648
665
|
return {
|
|
649
|
-
success: response.success,
|
|
650
|
-
challengePassed: response.
|
|
651
|
-
confidence: response.confidence,
|
|
652
|
-
remainingChallenges: response.
|
|
666
|
+
success: response.success ?? true,
|
|
667
|
+
challengePassed: response.challengePassed ?? response.completed ?? false,
|
|
668
|
+
confidence: response.confidence ?? response.score ?? 0,
|
|
669
|
+
remainingChallenges: response.remainingChallenges ?? 0
|
|
653
670
|
};
|
|
654
671
|
}
|
|
655
672
|
/**
|
|
656
|
-
* Check document quality before uploading
|
|
673
|
+
* Check document quality before uploading.
|
|
657
674
|
*/
|
|
658
675
|
async checkDocumentQuality(imageData, documentType) {
|
|
659
|
-
const
|
|
676
|
+
const documentFrontBase64 = await blobToBase64(imageData);
|
|
660
677
|
return this.request("/kyc/document-quality", {
|
|
661
678
|
method: "POST",
|
|
662
679
|
body: JSON.stringify({
|
|
663
|
-
|
|
664
|
-
|
|
680
|
+
documentFrontBase64,
|
|
681
|
+
documentType
|
|
665
682
|
})
|
|
666
683
|
});
|
|
667
684
|
}
|
|
@@ -710,7 +727,7 @@ var ApiClient = class {
|
|
|
710
727
|
}
|
|
711
728
|
headers.set("X-Tenant-ID", this.configuration.tenantId);
|
|
712
729
|
headers.set("Accept", "application/json");
|
|
713
|
-
if (!
|
|
730
|
+
if (!headers.has("Content-Type")) {
|
|
714
731
|
headers.set("Content-Type", "application/json");
|
|
715
732
|
}
|
|
716
733
|
const requestOptions = {
|
|
@@ -781,20 +798,13 @@ var ApiClient = class {
|
|
|
781
798
|
sleep(ms) {
|
|
782
799
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
783
800
|
}
|
|
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
801
|
/**
|
|
797
|
-
* Transform snake_case response to camelCase
|
|
802
|
+
* Transform snake_case response to camelCase.
|
|
803
|
+
*
|
|
804
|
+
* Some backend endpoints still return snake_case keys; the
|
|
805
|
+
* domain types in ApiModels.ts are camelCase. This walker turns
|
|
806
|
+
* `external_id` → `externalId` etc. recursively. Safe no-op for
|
|
807
|
+
* already-camelCase responses.
|
|
798
808
|
*/
|
|
799
809
|
transformResponse(data) {
|
|
800
810
|
if (Array.isArray(data)) {
|
|
@@ -811,6 +821,24 @@ var ApiClient = class {
|
|
|
811
821
|
return data;
|
|
812
822
|
}
|
|
813
823
|
};
|
|
824
|
+
function instructionForChallengeType(type) {
|
|
825
|
+
switch (type) {
|
|
826
|
+
case "blink":
|
|
827
|
+
return "Blink your eyes slowly";
|
|
828
|
+
case "smile":
|
|
829
|
+
return "Smile naturally";
|
|
830
|
+
case "turn_left":
|
|
831
|
+
return "Slowly turn your head to the left";
|
|
832
|
+
case "turn_right":
|
|
833
|
+
return "Slowly turn your head to the right";
|
|
834
|
+
case "nod_up":
|
|
835
|
+
return "Slowly tilt your head up";
|
|
836
|
+
case "nod_down":
|
|
837
|
+
return "Slowly tilt your head down";
|
|
838
|
+
default:
|
|
839
|
+
return "Follow the on-screen prompt";
|
|
840
|
+
}
|
|
841
|
+
}
|
|
814
842
|
|
|
815
843
|
// src/KoraIDV.ts
|
|
816
844
|
var KoraIDV = class {
|
|
@@ -826,7 +854,7 @@ var KoraIDV = class {
|
|
|
826
854
|
this.apiClient = new ApiClient(this.configuration);
|
|
827
855
|
}
|
|
828
856
|
detectEnvironment(apiKey) {
|
|
829
|
-
return apiKey.startsWith("
|
|
857
|
+
return apiKey.startsWith("sk_sandbox_") ? "sandbox" : "production";
|
|
830
858
|
}
|
|
831
859
|
/**
|
|
832
860
|
* Get supported countries and their document types from the API
|
package/dist/index.mjs
CHANGED
|
@@ -506,7 +506,7 @@ var ApiClient = class {
|
|
|
506
506
|
return this.request("/verifications", {
|
|
507
507
|
method: "POST",
|
|
508
508
|
body: JSON.stringify({
|
|
509
|
-
|
|
509
|
+
externalId: request.externalId,
|
|
510
510
|
tier: request.tier
|
|
511
511
|
})
|
|
512
512
|
});
|
|
@@ -520,23 +520,23 @@ var ApiClient = class {
|
|
|
520
520
|
/**
|
|
521
521
|
* Upload document image.
|
|
522
522
|
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
523
|
+
* Both sides now use the same JSON wire format as the Android/iOS SDKs.
|
|
524
|
+
* Front sends documentType + optional country; back sends optional
|
|
525
|
+
* decodedBarcodePayload (Phase 4 fast-path — when the browser's
|
|
526
|
+
* BarcodeDetector decoded the PDF417 on-device, the AAMVA payload
|
|
527
|
+
* travels here so the server can skip image-based barcode decoding).
|
|
528
|
+
*
|
|
529
|
+
* The previous front-side multipart path returned HTTP 400 on every
|
|
530
|
+
* request because the server's /document handler only parses
|
|
531
|
+
* application/json — same trap iOS hit and fixed in v1.6.0.
|
|
531
532
|
*/
|
|
532
|
-
async uploadDocument(verificationId, imageData, side, documentType, decodedBarcodePayload) {
|
|
533
|
+
async uploadDocument(verificationId, imageData, side, documentType, decodedBarcodePayload, country) {
|
|
534
|
+
const imageBase64 = await blobToBase64(imageData);
|
|
533
535
|
if (side === "back") {
|
|
534
|
-
const imageBase64 = await blobToBase64(imageData);
|
|
535
536
|
return this.request(
|
|
536
537
|
`/verifications/${verificationId}/document/back`,
|
|
537
538
|
{
|
|
538
539
|
method: "POST",
|
|
539
|
-
headers: { "Content-Type": "application/json" },
|
|
540
540
|
body: JSON.stringify({
|
|
541
541
|
imageBase64,
|
|
542
542
|
decodedBarcodePayload: decodedBarcodePayload ?? null
|
|
@@ -544,80 +544,97 @@ var ApiClient = class {
|
|
|
544
544
|
}
|
|
545
545
|
);
|
|
546
546
|
}
|
|
547
|
-
const formData = new FormData();
|
|
548
|
-
formData.append("image", imageData, "document.jpg");
|
|
549
|
-
formData.append("document_type", documentType);
|
|
550
|
-
formData.append("side", side);
|
|
551
547
|
return this.request(
|
|
552
548
|
`/verifications/${verificationId}/document`,
|
|
553
549
|
{
|
|
554
550
|
method: "POST",
|
|
555
|
-
body:
|
|
556
|
-
|
|
557
|
-
|
|
551
|
+
body: JSON.stringify({
|
|
552
|
+
documentType,
|
|
553
|
+
imageBase64,
|
|
554
|
+
country: country ?? null
|
|
555
|
+
})
|
|
558
556
|
}
|
|
559
557
|
);
|
|
560
558
|
}
|
|
561
559
|
/**
|
|
562
|
-
* Upload selfie image
|
|
560
|
+
* Upload selfie image.
|
|
561
|
+
*
|
|
562
|
+
* JSON with imageBase64 — matches the server's UploadSelfieRequest
|
|
563
|
+
* { ImageBase64 string } and the Android/iOS wire format. Previous
|
|
564
|
+
* multipart path returned HTTP 400 since the SDK shipped.
|
|
563
565
|
*/
|
|
564
566
|
async uploadSelfie(verificationId, imageData) {
|
|
565
|
-
const
|
|
566
|
-
formData.append("image", imageData, "selfie.jpg");
|
|
567
|
+
const imageBase64 = await blobToBase64(imageData);
|
|
567
568
|
return this.request(`/verifications/${verificationId}/selfie`, {
|
|
568
569
|
method: "POST",
|
|
569
|
-
body:
|
|
570
|
-
headers: {}
|
|
570
|
+
body: JSON.stringify({ imageBase64 })
|
|
571
571
|
});
|
|
572
572
|
}
|
|
573
573
|
/**
|
|
574
|
-
* Create liveness session
|
|
574
|
+
* Create liveness session.
|
|
575
|
+
*
|
|
576
|
+
* Decodes the wire DTO (matches backend's `models.LivenessSession`
|
|
577
|
+
* JSON), then projects it onto the domain `LivenessSession` the UI
|
|
578
|
+
* expects. Backend doesn't send `sessionId`/`expiresAt`, and per-
|
|
579
|
+
* challenge `id`/`instruction`/`order` aren't on the wire — domain
|
|
580
|
+
* values are synthesized client-side. Mirrors the iOS v1.7.0 pattern
|
|
581
|
+
* at koraidv-ios/.../SessionManager.swift::createLivenessSession.
|
|
582
|
+
*
|
|
583
|
+
* Without this DTO/domain split, the SDK accessed undefined fields
|
|
584
|
+
* on the response and produced a session with sessionId=undefined,
|
|
585
|
+
* which then failed every downstream challenge submission.
|
|
575
586
|
*/
|
|
576
587
|
async createLivenessSession(verificationId) {
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
588
|
+
const dto = await this.request(
|
|
589
|
+
`/verifications/${verificationId}/liveness/session`,
|
|
590
|
+
{ method: "POST" }
|
|
591
|
+
);
|
|
580
592
|
return {
|
|
581
|
-
sessionId:
|
|
582
|
-
challenges:
|
|
583
|
-
id:
|
|
593
|
+
sessionId: dto.id,
|
|
594
|
+
challenges: dto.challenges.map((c, index) => ({
|
|
595
|
+
id: `${dto.id}_${index}`,
|
|
584
596
|
type: c.type,
|
|
585
|
-
instruction: c.
|
|
586
|
-
order:
|
|
597
|
+
instruction: instructionForChallengeType(c.type),
|
|
598
|
+
order: index
|
|
587
599
|
})),
|
|
588
|
-
expiresAt
|
|
600
|
+
// Backend doesn't return expiresAt; enforce a local 5-minute
|
|
601
|
+
// timeout consistent with the Android + iOS peers.
|
|
602
|
+
expiresAt: new Date(Date.now() + 5 * 60 * 1e3)
|
|
589
603
|
};
|
|
590
604
|
}
|
|
591
605
|
/**
|
|
592
|
-
* Submit liveness challenge
|
|
606
|
+
* Submit liveness challenge result.
|
|
607
|
+
*
|
|
608
|
+
* JSON with challengeType + imageBase64 — matches the server's
|
|
609
|
+
* SubmitLivenessChallengeRequest and the Android/iOS wire format.
|
|
610
|
+
* Previous multipart + snake_case path was double-broken.
|
|
593
611
|
*/
|
|
594
612
|
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);
|
|
613
|
+
const imageBase64 = await blobToBase64(imageData);
|
|
599
614
|
const response = await this.request(`/verifications/${verificationId}/liveness/challenge`, {
|
|
600
615
|
method: "POST",
|
|
601
|
-
body:
|
|
602
|
-
|
|
616
|
+
body: JSON.stringify({
|
|
617
|
+
challengeType: challenge.type,
|
|
618
|
+
imageBase64
|
|
619
|
+
})
|
|
603
620
|
});
|
|
604
621
|
return {
|
|
605
|
-
success: response.success,
|
|
606
|
-
challengePassed: response.
|
|
607
|
-
confidence: response.confidence,
|
|
608
|
-
remainingChallenges: response.
|
|
622
|
+
success: response.success ?? true,
|
|
623
|
+
challengePassed: response.challengePassed ?? response.completed ?? false,
|
|
624
|
+
confidence: response.confidence ?? response.score ?? 0,
|
|
625
|
+
remainingChallenges: response.remainingChallenges ?? 0
|
|
609
626
|
};
|
|
610
627
|
}
|
|
611
628
|
/**
|
|
612
|
-
* Check document quality before uploading
|
|
629
|
+
* Check document quality before uploading.
|
|
613
630
|
*/
|
|
614
631
|
async checkDocumentQuality(imageData, documentType) {
|
|
615
|
-
const
|
|
632
|
+
const documentFrontBase64 = await blobToBase64(imageData);
|
|
616
633
|
return this.request("/kyc/document-quality", {
|
|
617
634
|
method: "POST",
|
|
618
635
|
body: JSON.stringify({
|
|
619
|
-
|
|
620
|
-
|
|
636
|
+
documentFrontBase64,
|
|
637
|
+
documentType
|
|
621
638
|
})
|
|
622
639
|
});
|
|
623
640
|
}
|
|
@@ -666,7 +683,7 @@ var ApiClient = class {
|
|
|
666
683
|
}
|
|
667
684
|
headers.set("X-Tenant-ID", this.configuration.tenantId);
|
|
668
685
|
headers.set("Accept", "application/json");
|
|
669
|
-
if (!
|
|
686
|
+
if (!headers.has("Content-Type")) {
|
|
670
687
|
headers.set("Content-Type", "application/json");
|
|
671
688
|
}
|
|
672
689
|
const requestOptions = {
|
|
@@ -737,20 +754,13 @@ var ApiClient = class {
|
|
|
737
754
|
sleep(ms) {
|
|
738
755
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
739
756
|
}
|
|
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
757
|
/**
|
|
753
|
-
* Transform snake_case response to camelCase
|
|
758
|
+
* Transform snake_case response to camelCase.
|
|
759
|
+
*
|
|
760
|
+
* Some backend endpoints still return snake_case keys; the
|
|
761
|
+
* domain types in ApiModels.ts are camelCase. This walker turns
|
|
762
|
+
* `external_id` → `externalId` etc. recursively. Safe no-op for
|
|
763
|
+
* already-camelCase responses.
|
|
754
764
|
*/
|
|
755
765
|
transformResponse(data) {
|
|
756
766
|
if (Array.isArray(data)) {
|
|
@@ -767,6 +777,24 @@ var ApiClient = class {
|
|
|
767
777
|
return data;
|
|
768
778
|
}
|
|
769
779
|
};
|
|
780
|
+
function instructionForChallengeType(type) {
|
|
781
|
+
switch (type) {
|
|
782
|
+
case "blink":
|
|
783
|
+
return "Blink your eyes slowly";
|
|
784
|
+
case "smile":
|
|
785
|
+
return "Smile naturally";
|
|
786
|
+
case "turn_left":
|
|
787
|
+
return "Slowly turn your head to the left";
|
|
788
|
+
case "turn_right":
|
|
789
|
+
return "Slowly turn your head to the right";
|
|
790
|
+
case "nod_up":
|
|
791
|
+
return "Slowly tilt your head up";
|
|
792
|
+
case "nod_down":
|
|
793
|
+
return "Slowly tilt your head down";
|
|
794
|
+
default:
|
|
795
|
+
return "Follow the on-screen prompt";
|
|
796
|
+
}
|
|
797
|
+
}
|
|
770
798
|
|
|
771
799
|
// src/KoraIDV.ts
|
|
772
800
|
var KoraIDV = class {
|
|
@@ -782,7 +810,7 @@ var KoraIDV = class {
|
|
|
782
810
|
this.apiClient = new ApiClient(this.configuration);
|
|
783
811
|
}
|
|
784
812
|
detectEnvironment(apiKey) {
|
|
785
|
-
return apiKey.startsWith("
|
|
813
|
+
return apiKey.startsWith("sk_sandbox_") ? "sandbox" : "production";
|
|
786
814
|
}
|
|
787
815
|
/**
|
|
788
816
|
* Get supported countries and their document types from the API
|