verant_id_cloud_scan 1.4.5-beta.9 → 1.4.6-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish_library.yml +9 -1
- package/Api.js +362 -67
- package/Api.test.js +338 -0
- package/CHANGELOG.md +151 -1
- package/CloudScan.js +76 -53
- package/LocalScannerApi.js +47 -32
- package/ScannerApi.js +38 -1
- package/WebSocketClient.js +85 -13
- package/index.d.ts +83 -0
- package/package.json +5 -2
|
@@ -24,4 +24,12 @@ jobs:
|
|
|
24
24
|
run: npm install -g npm@latest
|
|
25
25
|
|
|
26
26
|
- name: Publish to npm
|
|
27
|
-
run:
|
|
27
|
+
run: |
|
|
28
|
+
VERSION="$(node -p "require('./package.json').version")"
|
|
29
|
+
if [[ "$VERSION" == *-* ]]; then
|
|
30
|
+
echo "Publishing prerelease $VERSION under the 'beta' dist-tag"
|
|
31
|
+
npm publish --tag beta
|
|
32
|
+
else
|
|
33
|
+
echo "Publishing release $VERSION under 'latest'"
|
|
34
|
+
npm publish
|
|
35
|
+
fi
|
package/Api.js
CHANGED
|
@@ -264,9 +264,60 @@ export const checkFaceComparisonFeatureLicense = async (clientId) => {
|
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
-
export const
|
|
267
|
+
export const checkDigitalIDFeatureLicense = async (clientId) => {
|
|
268
|
+
const url = getLicenseServerAddress() + "/mobile_id/check_license";
|
|
269
|
+
const data = { client_id: clientId };
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const response = await fetch(url, {
|
|
273
|
+
method: "POST",
|
|
274
|
+
headers: {
|
|
275
|
+
"Content-Type": "application/json",
|
|
276
|
+
},
|
|
277
|
+
body: JSON.stringify(data),
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Extract the JSON data from the response
|
|
281
|
+
const responseData = await response.json();
|
|
282
|
+
|
|
283
|
+
if (response.ok && responseData.licensed === true) {
|
|
284
|
+
return true; // Successful response
|
|
285
|
+
} else {
|
|
286
|
+
console.log(
|
|
287
|
+
"checkDigitalIDFeatureLicense returned false or non 200:",
|
|
288
|
+
responseData
|
|
289
|
+
);
|
|
290
|
+
return false; // Unsuccessful response
|
|
291
|
+
}
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.error(
|
|
294
|
+
"There was a problem with the checkDigitalIDFeatureLicense request:",
|
|
295
|
+
error
|
|
296
|
+
);
|
|
297
|
+
return false; // Error occurred
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// Pass exactly one of macAddress / serialNumber. The license server rejects
|
|
302
|
+
// requests that supply both or neither; we also fail fast here so callers
|
|
303
|
+
// don't have to wait for a 400 to learn they passed a bad identifier.
|
|
304
|
+
export const checkLicense = async (clientId, identifier = {}) => {
|
|
305
|
+
if (identifier === null || typeof identifier !== "object") {
|
|
306
|
+
throw new TypeError(
|
|
307
|
+
`checkLicense: second argument must be an object like { macAddress } or { serialNumber }, got ${typeof identifier}`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
const { macAddress, serialNumber } = identifier;
|
|
311
|
+
if (Boolean(macAddress) === Boolean(serialNumber)) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
"checkLicense: pass exactly one of macAddress / serialNumber",
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
268
317
|
const url = getLicenseServerAddress() + "/check_license";
|
|
269
|
-
const data = { client_id: clientId
|
|
318
|
+
const data = { client_id: clientId };
|
|
319
|
+
if (macAddress) data.mac_address = macAddress;
|
|
320
|
+
if (serialNumber) data.serial_number = serialNumber;
|
|
270
321
|
|
|
271
322
|
try {
|
|
272
323
|
const response = await fetch(url, {
|
|
@@ -346,48 +397,148 @@ export const getBarcodeLicenseKey = async () => {
|
|
|
346
397
|
}
|
|
347
398
|
};
|
|
348
399
|
|
|
349
|
-
|
|
350
|
-
|
|
400
|
+
/**
|
|
401
|
+
* Build the /securelink URL for the given scanner address.
|
|
402
|
+
*
|
|
403
|
+
* Mirrors the SDK's historical behaviour: if the caller didn't include a
|
|
404
|
+
* scheme, default to http://. The scheme test is case-insensitive per
|
|
405
|
+
* RFC 3986 (VNT-1522) — `HTTP://`, `HTTPS://`, and mixed-case forms are
|
|
406
|
+
* recognised so the SDK doesn't double-prefix them with another `http://`.
|
|
407
|
+
*/
|
|
408
|
+
export const buildSecurelinkUrl = (scannerAddress) => {
|
|
409
|
+
const hasScheme = /^https?:\/\//i.test(scannerAddress);
|
|
410
|
+
const withScheme = hasScheme ? scannerAddress : `http://${scannerAddress}`;
|
|
411
|
+
return `${withScheme.replace(/\/+$/, "")}/securelink`;
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Lower-level POST that returns a structured result instead of throwing.
|
|
416
|
+
* Used by `checkScannerPresentDetailed` (VNT-1522) so the caller can
|
|
417
|
+
* distinguish network failures, HTTP errors, parse failures, and scanner
|
|
418
|
+
* application-layer errors. Existing callers go through `postToScannerApi`
|
|
419
|
+
* which preserves the throw-on-error contract.
|
|
420
|
+
*
|
|
421
|
+
* Supports an external `AbortSignal` so host apps can cancel an in-flight
|
|
422
|
+
* probe on retry / unmount without waiting for the internal timeout.
|
|
423
|
+
*
|
|
424
|
+
* @param {string} scannerAddress
|
|
425
|
+
* @param {object} commandObj
|
|
426
|
+
* @param {number} [timeout=10000] timeout in ms before the internal AbortController aborts
|
|
427
|
+
* @param {AbortSignal} [externalSignal] optional external signal; when aborted, the request is cancelled
|
|
428
|
+
* @returns {Promise<{
|
|
429
|
+
* threw: boolean,
|
|
430
|
+
* error?: Error,
|
|
431
|
+
* ok?: boolean,
|
|
432
|
+
* status?: number,
|
|
433
|
+
* statusText?: string,
|
|
434
|
+
* body?: unknown,
|
|
435
|
+
* attemptedUrl: string,
|
|
436
|
+
* }>}
|
|
437
|
+
*/
|
|
438
|
+
const postToScannerApiDetailed = async (
|
|
351
439
|
scannerAddress,
|
|
352
440
|
commandObj,
|
|
353
|
-
timeout = 10000
|
|
441
|
+
timeout = 10000,
|
|
442
|
+
externalSignal,
|
|
354
443
|
) => {
|
|
355
|
-
|
|
356
|
-
let formattedAddress = scannerAddress;
|
|
357
|
-
if (!scannerAddress.startsWith('http://') && !scannerAddress.startsWith('https://')) {
|
|
358
|
-
formattedAddress = `http://${scannerAddress}`;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Remove trailing slash to prevent double slashes in URL
|
|
362
|
-
formattedAddress = formattedAddress.replace(/\/+$/, '');
|
|
363
|
-
|
|
364
|
-
let apiAddress = `${formattedAddress}/securelink`;
|
|
444
|
+
const attemptedUrl = buildSecurelinkUrl(scannerAddress);
|
|
365
445
|
|
|
366
446
|
const controller = new AbortController();
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
447
|
+
const fetchTimeout = setTimeout(() => controller.abort(), timeout);
|
|
448
|
+
|
|
449
|
+
// Forward an external abort signal to our controller so the host app's
|
|
450
|
+
// cancellation (retry, unmount) propagates to the in-flight fetch.
|
|
451
|
+
let externalAbortHandler;
|
|
452
|
+
if (externalSignal) {
|
|
453
|
+
if (externalSignal.aborted) {
|
|
454
|
+
controller.abort();
|
|
455
|
+
} else {
|
|
456
|
+
externalAbortHandler = () => controller.abort();
|
|
457
|
+
externalSignal.addEventListener("abort", externalAbortHandler, { once: true });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
372
460
|
|
|
373
461
|
try {
|
|
374
|
-
const response = await fetch(
|
|
462
|
+
const response = await fetch(attemptedUrl, {
|
|
375
463
|
method: "POST",
|
|
376
464
|
headers: { "Content-Type": "application/json" },
|
|
377
465
|
body: JSON.stringify(commandObj),
|
|
378
|
-
signal: signal,
|
|
466
|
+
signal: controller.signal,
|
|
379
467
|
});
|
|
380
468
|
|
|
381
|
-
|
|
469
|
+
// Read the body once as text, then try to JSON-parse — keeps a malformed
|
|
470
|
+
// body from making us discard otherwise-useful status info.
|
|
471
|
+
let body = null;
|
|
472
|
+
try {
|
|
473
|
+
const text = await response.text();
|
|
474
|
+
if (text) {
|
|
475
|
+
try {
|
|
476
|
+
body = JSON.parse(text);
|
|
477
|
+
} catch {
|
|
478
|
+
body = text; // not JSON, but still meaningful for diagnostics
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
} catch {
|
|
482
|
+
// body read failed — leave body=null, caller can decide
|
|
483
|
+
}
|
|
382
484
|
|
|
383
|
-
|
|
384
|
-
|
|
485
|
+
return {
|
|
486
|
+
threw: false,
|
|
487
|
+
ok: response.ok,
|
|
488
|
+
status: response.status,
|
|
489
|
+
statusText: response.statusText,
|
|
490
|
+
body,
|
|
491
|
+
attemptedUrl,
|
|
492
|
+
};
|
|
385
493
|
} catch (error) {
|
|
386
|
-
|
|
494
|
+
return { threw: true, error, attemptedUrl };
|
|
495
|
+
} finally {
|
|
496
|
+
clearTimeout(fetchTimeout);
|
|
497
|
+
if (externalSignal && externalAbortHandler) {
|
|
498
|
+
externalSignal.removeEventListener("abort", externalAbortHandler);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// Helper function for making POST requests to the scanner API. Thin wrapper
|
|
504
|
+
// around `postToScannerApiDetailed` that preserves the historical contract
|
|
505
|
+
// (throws on timeout / network / non-2xx; returns parsed JSON on success).
|
|
506
|
+
const postToScannerApi = async (
|
|
507
|
+
scannerAddress,
|
|
508
|
+
commandObj,
|
|
509
|
+
timeout = 10000
|
|
510
|
+
) => {
|
|
511
|
+
const result = await postToScannerApiDetailed(
|
|
512
|
+
scannerAddress,
|
|
513
|
+
commandObj,
|
|
514
|
+
timeout,
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
if (result.threw) {
|
|
518
|
+
if (result.error?.name === "AbortError") {
|
|
387
519
|
throw new Error("Request timed out");
|
|
388
520
|
}
|
|
389
|
-
throw error;
|
|
521
|
+
throw result.error;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (!result.ok) {
|
|
525
|
+
throw new Error(result.statusText || `HTTP ${result.status}`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Match the original `response.json()` contract: throw if the 2xx body
|
|
529
|
+
// wasn't a parseable JSON object. The detailed wrapper deliberately
|
|
530
|
+
// preserves non-JSON bodies as strings (or `null` for empty) so error
|
|
531
|
+
// surfaces like `checkScannerPresentDetailed` can use them — but every
|
|
532
|
+
// existing caller of `postToScannerApi` (`getNetworkInfo`,
|
|
533
|
+
// `establishConnection`, `getParams`, `resetParams`, `setParameters`,
|
|
534
|
+
// `scanDocument`, …) reads object properties off the return value, so
|
|
535
|
+
// returning a string or null here would silently produce `undefined`
|
|
536
|
+
// downstream instead of failing loudly.
|
|
537
|
+
if (result.body === null || typeof result.body !== "object") {
|
|
538
|
+
throw new SyntaxError("Scanner response was not a JSON object");
|
|
390
539
|
}
|
|
540
|
+
|
|
541
|
+
return result.body;
|
|
391
542
|
};
|
|
392
543
|
|
|
393
544
|
export const getNetworkInfo = (scannerAddress, commandObj) =>
|
|
@@ -414,42 +565,174 @@ export const stopScanning = (scannerAddress, commandObj) =>
|
|
|
414
565
|
export const closeConnection = (scannerAddress, commandObj) =>
|
|
415
566
|
postToScannerApi(scannerAddress, commandObj);
|
|
416
567
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
568
|
+
/**
|
|
569
|
+
* Classify a thrown fetch error into one of the documented codes (VNT-1522).
|
|
570
|
+
*
|
|
571
|
+
* `externallyAborted` distinguishes a host-app cancellation (retry, unmount)
|
|
572
|
+
* from an internal timeout — both surface as `AbortError` from fetch, but
|
|
573
|
+
* callers branching on `code` for UI copy (e.g. "scanner timed out") should
|
|
574
|
+
* not see the timeout label when the user themselves cancelled.
|
|
575
|
+
*
|
|
576
|
+
* @param {unknown} error
|
|
577
|
+
* @param {string} attemptedUrl
|
|
578
|
+
* @param {boolean} [externallyAborted=false]
|
|
579
|
+
* @returns {{ code: string, name: string, message: string, attemptedUrl: string }}
|
|
580
|
+
*/
|
|
581
|
+
const classifyFetchError = (error, attemptedUrl, externallyAborted = false) => {
|
|
582
|
+
if (error?.name === "AbortError") {
|
|
583
|
+
return {
|
|
584
|
+
code: externallyAborted ? "ABORTED" : "TIMEOUT",
|
|
585
|
+
name: "AbortError",
|
|
586
|
+
message: externallyAborted ? "Request aborted" : "Request timed out",
|
|
587
|
+
attemptedUrl,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
if (error instanceof TypeError) {
|
|
591
|
+
// Browser fetch surfaces network failures (DNS, connection refused,
|
|
592
|
+
// mixed-content blocks, CORS preflight rejection) as `TypeError`.
|
|
593
|
+
return {
|
|
594
|
+
code: "NETWORK_ERROR",
|
|
595
|
+
name: "TypeError",
|
|
596
|
+
message: error.message || "Network error",
|
|
597
|
+
attemptedUrl,
|
|
429
598
|
};
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
code: "NETWORK_ERROR",
|
|
602
|
+
name: error?.name || "Unknown",
|
|
603
|
+
message: error?.message || String(error),
|
|
604
|
+
attemptedUrl,
|
|
605
|
+
};
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Structured version of `checkScannerPresent` (VNT-1522).
|
|
610
|
+
*
|
|
611
|
+
* Returns `{ ok: true }` on success or `{ ok: false, error }` with a tagged
|
|
612
|
+
* `error.code` so host apps can distinguish:
|
|
613
|
+
* - `TIMEOUT` — internal 5s timeout fired
|
|
614
|
+
* - `ABORTED` — host app cancelled via the external `AbortSignal`
|
|
615
|
+
* - `NETWORK_ERROR` — fetch threw (DNS, refused, mixed content, CORS, etc.)
|
|
616
|
+
* - `HTTP_ERROR` — server replied non-2xx
|
|
617
|
+
* - `INVALID_RESPONSE` — 2xx but the body wasn't JSON
|
|
618
|
+
* - `SCANNER_ERROR` — 2xx JSON but no `SessionId` (the scanner returned
|
|
619
|
+
* an application-layer error in the body, e.g. an HTTPS-only scanner
|
|
620
|
+
* rejecting an HTTP probe with `{HttpRC:"403", ExtInformation:"..."}`)
|
|
621
|
+
*
|
|
622
|
+
* The error object also carries `attemptedUrl`, `status` (when a response
|
|
623
|
+
* was received), and the raw `scannerResponse` body for SCANNER_ERROR so
|
|
624
|
+
* callers can surface vendor-specific fields like `ExtInformation`.
|
|
625
|
+
*
|
|
626
|
+
* @param {string} scannerAddress
|
|
627
|
+
* @param {{ signal?: AbortSignal }} [options]
|
|
628
|
+
*/
|
|
629
|
+
export const checkScannerPresentDetailed = async (
|
|
630
|
+
scannerAddress,
|
|
631
|
+
options = {},
|
|
632
|
+
) => {
|
|
633
|
+
const { signal } = options;
|
|
634
|
+
const attemptedUrl = buildSecurelinkUrl(scannerAddress);
|
|
635
|
+
|
|
636
|
+
const connectCommand = {
|
|
637
|
+
Command: "Connect",
|
|
638
|
+
MessageId: new Date().toISOString(),
|
|
639
|
+
Exclusive: "false", // non-exclusive — we're only probing
|
|
640
|
+
IdleTimeout: "5",
|
|
641
|
+
};
|
|
430
642
|
|
|
431
|
-
|
|
643
|
+
const probe = await postToScannerApiDetailed(
|
|
644
|
+
scannerAddress,
|
|
645
|
+
connectCommand,
|
|
646
|
+
5000,
|
|
647
|
+
signal,
|
|
648
|
+
);
|
|
432
649
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
650
|
+
if (probe.threw) {
|
|
651
|
+
return {
|
|
652
|
+
ok: false,
|
|
653
|
+
error: classifyFetchError(
|
|
654
|
+
probe.error,
|
|
655
|
+
attemptedUrl,
|
|
656
|
+
Boolean(signal?.aborted),
|
|
657
|
+
),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
437
660
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
661
|
+
if (!probe.ok) {
|
|
662
|
+
return {
|
|
663
|
+
ok: false,
|
|
664
|
+
error: {
|
|
665
|
+
code: "HTTP_ERROR",
|
|
666
|
+
name: "HTTPError",
|
|
667
|
+
message: probe.statusText || `HTTP ${probe.status}`,
|
|
668
|
+
status: probe.status,
|
|
669
|
+
attemptedUrl,
|
|
670
|
+
},
|
|
443
671
|
};
|
|
672
|
+
}
|
|
444
673
|
|
|
445
|
-
|
|
674
|
+
if (!probe.body || typeof probe.body !== "object") {
|
|
675
|
+
return {
|
|
676
|
+
ok: false,
|
|
677
|
+
error: {
|
|
678
|
+
code: "INVALID_RESPONSE",
|
|
679
|
+
name: "InvalidResponse",
|
|
680
|
+
message: "Scanner returned a non-JSON or empty body",
|
|
681
|
+
status: probe.status,
|
|
682
|
+
attemptedUrl,
|
|
683
|
+
},
|
|
684
|
+
};
|
|
685
|
+
}
|
|
446
686
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
//
|
|
451
|
-
|
|
687
|
+
if (!probe.body.SessionId) {
|
|
688
|
+
// App-layer scanner error — common when an HTTPS-only scanner is hit
|
|
689
|
+
// over HTTP. The body usually contains vendor-specific fields like
|
|
690
|
+
// `ExtInformation` ("Scanner not configured for HTTP") that we forward
|
|
691
|
+
// so callers can present an actionable message.
|
|
692
|
+
return {
|
|
693
|
+
ok: false,
|
|
694
|
+
error: {
|
|
695
|
+
code: "SCANNER_ERROR",
|
|
696
|
+
name: "ScannerError",
|
|
697
|
+
message:
|
|
698
|
+
probe.body.ExtInformation ||
|
|
699
|
+
probe.body.ErrorInformation ||
|
|
700
|
+
"Scanner returned no SessionId",
|
|
701
|
+
status:
|
|
702
|
+
typeof probe.body.HttpRC === "string"
|
|
703
|
+
? parseInt(probe.body.HttpRC, 10) || probe.status
|
|
704
|
+
: probe.status,
|
|
705
|
+
attemptedUrl,
|
|
706
|
+
scannerResponse: probe.body,
|
|
707
|
+
},
|
|
708
|
+
};
|
|
452
709
|
}
|
|
710
|
+
|
|
711
|
+
// Got a SessionId — clean up by disconnecting. We don't surface a
|
|
712
|
+
// disconnect failure: from the host app's perspective the scanner IS
|
|
713
|
+
// present and the leaked session will time out server-side per the
|
|
714
|
+
// scanner's IdleTimeout.
|
|
715
|
+
const disconnectCommand = {
|
|
716
|
+
Command: "Disconnect",
|
|
717
|
+
MessageId: new Date().toISOString(),
|
|
718
|
+
SessionId: probe.body.SessionId,
|
|
719
|
+
};
|
|
720
|
+
await postToScannerApiDetailed(
|
|
721
|
+
scannerAddress,
|
|
722
|
+
disconnectCommand,
|
|
723
|
+
5000,
|
|
724
|
+
signal,
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
return { ok: true };
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
// Boolean form of scanner presence check. Thin wrapper around
|
|
731
|
+
// `checkScannerPresentDetailed` (VNT-1522) — preserved for existing
|
|
732
|
+
// callers; new code should prefer the detailed version.
|
|
733
|
+
export const checkScannerPresent = async (scannerAddress) => {
|
|
734
|
+
const result = await checkScannerPresentDetailed(scannerAddress);
|
|
735
|
+
return result.ok;
|
|
453
736
|
};
|
|
454
737
|
|
|
455
738
|
/**
|
|
@@ -460,11 +743,16 @@ export const checkScannerPresent = async (scannerAddress) => {
|
|
|
460
743
|
*
|
|
461
744
|
* @param {string} userId - User ID for the verification session
|
|
462
745
|
* @param {string} clientId - Client ID for licensing and tracking
|
|
746
|
+
* @param {string} [origin] - Holder page origin; binds the server's
|
|
747
|
+
* SessionTranscript to the wallet's view (HPKE info). Required on non-default hosts.
|
|
463
748
|
* @returns {Promise<Object>} Session data with session_id, session_url, and websocket_url
|
|
464
749
|
*/
|
|
465
|
-
export const createVerificationSession = async (userId, clientId) => {
|
|
750
|
+
export const createVerificationSession = async (userId, clientId, origin) => {
|
|
466
751
|
const url = getMobileVerificationServerAddress() + "/sessions";
|
|
467
752
|
const data = { user_id: userId, client_id: clientId };
|
|
753
|
+
if (origin) {
|
|
754
|
+
data.origin = origin;
|
|
755
|
+
}
|
|
468
756
|
|
|
469
757
|
try {
|
|
470
758
|
const response = await fetch(url, {
|
|
@@ -479,24 +767,31 @@ export const createVerificationSession = async (userId, clientId) => {
|
|
|
479
767
|
const responseData = await response.json();
|
|
480
768
|
return responseData;
|
|
481
769
|
} else {
|
|
482
|
-
//
|
|
770
|
+
// Read the body once as text — fetch consumes the stream on first read,
|
|
771
|
+
// so a `response.json()` then `response.text()` fallback would throw
|
|
772
|
+
// `TypeError: body stream already read` and mask the original error.
|
|
773
|
+
const errorText = await response.text();
|
|
774
|
+
let errorData = null;
|
|
483
775
|
try {
|
|
484
|
-
|
|
776
|
+
errorData = JSON.parse(errorText);
|
|
777
|
+
} catch {
|
|
778
|
+
// Non-JSON body (e.g. plain text 502 from API Gateway).
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (errorData) {
|
|
485
782
|
console.error("Verification session creation failed:", errorData);
|
|
486
783
|
return {
|
|
487
784
|
status: 'error',
|
|
488
785
|
message: errorData.message || 'Failed to create verification session',
|
|
489
|
-
error: errorData
|
|
490
|
-
};
|
|
491
|
-
} catch (parseError) {
|
|
492
|
-
const errorText = await response.text();
|
|
493
|
-
console.error("Verification session creation failed:", errorText);
|
|
494
|
-
return {
|
|
495
|
-
status: 'error',
|
|
496
|
-
message: 'Failed to create verification session',
|
|
497
|
-
raw_error: errorText
|
|
786
|
+
error: errorData,
|
|
498
787
|
};
|
|
499
788
|
}
|
|
789
|
+
console.error("Verification session creation failed:", errorText);
|
|
790
|
+
return {
|
|
791
|
+
status: 'error',
|
|
792
|
+
message: 'Failed to create verification session',
|
|
793
|
+
raw_error: errorText,
|
|
794
|
+
};
|
|
500
795
|
}
|
|
501
796
|
} catch (error) {
|
|
502
797
|
console.error("There was a problem with the verification session request:", error);
|