@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 CHANGED
@@ -66,7 +66,7 @@ declare function getDocumentTypeInfo(type: DocumentType): DocumentTypeInfo;
66
66
  * SDK Configuration
67
67
  */
68
68
  interface Configuration {
69
- /** API key (starts with ck_live_ or ck_sandbox_) */
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
- * `decodedBarcodePayload` is the optional Phase 4 fast-path: when the
533
- * client decoded the PDF417 / QR / DataMatrix on-device using the
534
- * browser's BarcodeDetector API (or a polyfill), the AAMVA payload
535
- * travels here so the server can skip image-based barcode decoding
536
- * (~1-3 s round-trip savings). Empty/`undefined` = server falls
537
- * back to its zxing-cpp + pdf417decoder cascade. Only meaningful for
538
- * back captures on documents that carry a barcode.
539
- * See `docs/architecture/idv-decode-roadmap.md` Phase 4.
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 (starts with ck_live_ or ck_sandbox_) */
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
- * `decodedBarcodePayload` is the optional Phase 4 fast-path: when the
533
- * client decoded the PDF417 / QR / DataMatrix on-device using the
534
- * browser's BarcodeDetector API (or a polyfill), the AAMVA payload
535
- * travels here so the server can skip image-based barcode decoding
536
- * (~1-3 s round-trip savings). Empty/`undefined` = server falls
537
- * back to its zxing-cpp + pdf417decoder cascade. Only meaningful for
538
- * back captures on documents that carry a barcode.
539
- * See `docs/architecture/idv-decode-roadmap.md` Phase 4.
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
- external_id: request.externalId,
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
- * `decodedBarcodePayload` is the optional Phase 4 fast-path: when the
568
- * client decoded the PDF417 / QR / DataMatrix on-device using the
569
- * browser's BarcodeDetector API (or a polyfill), the AAMVA payload
570
- * travels here so the server can skip image-based barcode decoding
571
- * (~1-3 s round-trip savings). Empty/`undefined` = server falls
572
- * back to its zxing-cpp + pdf417decoder cascade. Only meaningful for
573
- * back captures on documents that carry a barcode.
574
- * See `docs/architecture/idv-decode-roadmap.md` Phase 4.
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: formData,
600
- headers: {}
601
- // Let browser set Content-Type for FormData
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 formData = new FormData();
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: formData,
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 response = await this.request(`/verifications/${verificationId}/liveness/session`, {
622
- method: "POST"
623
- });
632
+ const dto = await this.request(
633
+ `/verifications/${verificationId}/liveness/session`,
634
+ { method: "POST" }
635
+ );
624
636
  return {
625
- sessionId: response.session_id,
626
- challenges: response.challenges.map((c) => ({
627
- id: c.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.instruction,
630
- order: c.order
641
+ instruction: instructionForChallengeType(c.type),
642
+ order: index
631
643
  })),
632
- expiresAt: new Date(response.expires_at)
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 formData = new FormData();
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: formData,
646
- headers: {}
660
+ body: JSON.stringify({
661
+ challengeType: challenge.type,
662
+ imageBase64
663
+ })
647
664
  });
648
665
  return {
649
- success: response.success,
650
- challengePassed: response.challenge_passed,
651
- confidence: response.confidence,
652
- remainingChallenges: response.remaining_challenges
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 base64 = await this.blobToBase64(imageData);
676
+ const documentFrontBase64 = await blobToBase64(imageData);
660
677
  return this.request("/kyc/document-quality", {
661
678
  method: "POST",
662
679
  body: JSON.stringify({
663
- document_front_base64: base64,
664
- document_type: documentType
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 (!(options.body instanceof FormData) && !headers.has("Content-Type")) {
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("ck_sandbox_") ? "sandbox" : "production";
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
- external_id: request.externalId,
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
- * `decodedBarcodePayload` is the optional Phase 4 fast-path: when the
524
- * client decoded the PDF417 / QR / DataMatrix on-device using the
525
- * browser's BarcodeDetector API (or a polyfill), the AAMVA payload
526
- * travels here so the server can skip image-based barcode decoding
527
- * (~1-3 s round-trip savings). Empty/`undefined` = server falls
528
- * back to its zxing-cpp + pdf417decoder cascade. Only meaningful for
529
- * back captures on documents that carry a barcode.
530
- * See `docs/architecture/idv-decode-roadmap.md` Phase 4.
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: formData,
556
- headers: {}
557
- // Let browser set Content-Type for FormData
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 formData = new FormData();
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: formData,
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 response = await this.request(`/verifications/${verificationId}/liveness/session`, {
578
- method: "POST"
579
- });
588
+ const dto = await this.request(
589
+ `/verifications/${verificationId}/liveness/session`,
590
+ { method: "POST" }
591
+ );
580
592
  return {
581
- sessionId: response.session_id,
582
- challenges: response.challenges.map((c) => ({
583
- id: c.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.instruction,
586
- order: c.order
597
+ instruction: instructionForChallengeType(c.type),
598
+ order: index
587
599
  })),
588
- expiresAt: new Date(response.expires_at)
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 formData = new FormData();
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: formData,
602
- headers: {}
616
+ body: JSON.stringify({
617
+ challengeType: challenge.type,
618
+ imageBase64
619
+ })
603
620
  });
604
621
  return {
605
- success: response.success,
606
- challengePassed: response.challenge_passed,
607
- confidence: response.confidence,
608
- remainingChallenges: response.remaining_challenges
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 base64 = await this.blobToBase64(imageData);
632
+ const documentFrontBase64 = await blobToBase64(imageData);
616
633
  return this.request("/kyc/document-quality", {
617
634
  method: "POST",
618
635
  body: JSON.stringify({
619
- document_front_base64: base64,
620
- document_type: documentType
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 (!(options.body instanceof FormData) && !headers.has("Content-Type")) {
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("ck_sandbox_") ? "sandbox" : "production";
813
+ return apiKey.startsWith("sk_sandbox_") ? "sandbox" : "production";
786
814
  }
787
815
  /**
788
816
  * Get supported countries and their document types from the API
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koraidv/core",
3
- "version": "1.5.5",
3
+ "version": "1.7.1",
4
4
  "description": "Kora IDV Core SDK - API Client and Utilities",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",