verant_id_cloud_scan 1.4.5 → 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.
@@ -24,4 +24,12 @@ jobs:
24
24
  run: npm install -g npm@latest
25
25
 
26
26
  - name: Publish to npm
27
- run: npm publish
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
@@ -3,17 +3,110 @@ import { formatDateToISO, DATE_FIELDS } from "./DateUtils.js";
3
3
  import { FIELD_MAPPING, FIELD_DEFAULTS, getFieldValue } from "./FieldMapping.js";
4
4
  import { sanitizeFieldForDmv } from "./FormatUtils.js";
5
5
 
6
- //prod
7
- const licenseServerAddress = "https://lic.verantid.com/api/v1";
8
- const dmvServerAddress = "https://dmv.verantid.com/api/v1/dmv_check";
9
- // staging
10
- // const licenseServerAddress =
11
- // "https://verant-license-server-staging-52b03b060a98.herokuapp.com/api/v1";
12
- // const dmvServerAddress =
13
- // "https://dmv-check-server-staging-fcaab48bec21.herokuapp.com/api/v1/dmv_check";
6
+ /**
7
+ * Environment Detection & Management
8
+ *
9
+ * Defaults to PRODUCTION for all hosts (including localhost).
10
+ * Only verify-staging.verantid.com automatically uses staging.
11
+ * Host apps can explicitly override via setEnvironment('staging' | 'production').
12
+ */
13
+
14
+ // Current environment - starts as null to trigger detection on first use
15
+ let currentEnvironment = null;
16
+
17
+ /**
18
+ * Detect environment based on hostname
19
+ * @returns {'staging' | 'production'}
20
+ */
21
+ function detectEnvironment() {
22
+ // Default to production for all hosts
23
+ let environment = 'production';
24
+
25
+ // Only auto-switch to staging for VerantID's staging domain
26
+ if (typeof window !== 'undefined') {
27
+ const hostname = window.location.hostname;
28
+
29
+ // ONLY verify-staging.verantid.com uses staging by default
30
+ if (hostname === 'verify-staging.verantid.com') {
31
+ environment = 'staging';
32
+ }
33
+
34
+ // All other hostnames (localhost, customer sites, verify.verantid.com, etc.) use production
35
+ }
36
+
37
+ console.log('[CloudScan] Environment detected:', environment, 'on', typeof window !== 'undefined' ? window.location.hostname : 'server');
38
+ return environment;
39
+ }
40
+
41
+ /**
42
+ * Get current environment (detects if not already set)
43
+ * @returns {'staging' | 'production'}
44
+ */
45
+ function getEnvironment() {
46
+ if (currentEnvironment === null) {
47
+ currentEnvironment = detectEnvironment();
48
+ }
49
+ return currentEnvironment;
50
+ }
51
+
52
+ /**
53
+ * Explicitly set the environment
54
+ * @param {'staging' | 'production'} environment
55
+ * @throws {Error} If environment is not 'staging' or 'production'
56
+ */
57
+ export function setEnvironment(environment) {
58
+ if (environment !== 'staging' && environment !== 'production') {
59
+ throw new Error(`Invalid environment: "${environment}". Must be "staging" or "production".`);
60
+ }
61
+ currentEnvironment = environment;
62
+ }
63
+
64
+ /**
65
+ * Get license server address based on current environment
66
+ * @returns {string}
67
+ */
68
+ function getLicenseServerAddress() {
69
+ const env = getEnvironment();
70
+ return env === 'staging'
71
+ ? "https://lic-staging.verantid.com/api/v1"
72
+ : "https://lic.verantid.com/api/v1";
73
+ }
74
+
75
+ /**
76
+ * Get DMV server address based on current environment
77
+ * @returns {string}
78
+ */
79
+ function getDmvServerAddress() {
80
+ const env = getEnvironment();
81
+ return env === 'staging'
82
+ ? "https://dmv-check-server-staging-fcaab48bec21.herokuapp.com/api/v1/dmv_check"
83
+ : "https://dmv.verantid.com/api/v1/dmv_check";
84
+ }
85
+
86
+ /**
87
+ * Get Face Comparison server address based on current environment
88
+ * @returns {string}
89
+ */
90
+ export function getFaceComparisonServerAddress() {
91
+ const env = getEnvironment();
92
+ return env === 'staging'
93
+ ? "https://staging.face.verantid.bluerocket.us/compare_id_and_face"
94
+ : "https://faceid.verantid.com/compare_id_and_face";
95
+ }
96
+
97
+ /**
98
+ * Get Mobile Verification server address based on current environment
99
+ * @returns {string}
100
+ */
101
+ function getMobileVerificationServerAddress() {
102
+ const env = getEnvironment();
103
+ return env === 'staging'
104
+ ? "https://staging.mdl.verantid.bluerocket.us"
105
+ : "https://mdl.verantid.bluerocket.us";
106
+ }
14
107
 
15
108
  export const dmvCheck = async (clientId, scannerType, licenseData) => {
16
- const url = dmvServerAddress;
109
+ const url = getDmvServerAddress();
17
110
 
18
111
  // Build the driver object dynamically.
19
112
  const driver = {};
@@ -104,7 +197,7 @@ export const dmvCheck = async (clientId, scannerType, licenseData) => {
104
197
  };
105
198
 
106
199
  export const checkDmvFeatureLicense = async (clientId) => {
107
- const url = licenseServerAddress + "/dmv_verifications/check_license";
200
+ const url = getLicenseServerAddress() + "/dmv_verifications/check_license";
108
201
  const data = { client_id: clientId };
109
202
 
110
203
  try {
@@ -138,7 +231,7 @@ export const checkDmvFeatureLicense = async (clientId) => {
138
231
  };
139
232
 
140
233
  export const checkFaceComparisonFeatureLicense = async (clientId) => {
141
- const url = licenseServerAddress + "/facial_scans/check_license";
234
+ const url = getLicenseServerAddress() + "/facial_scans/check_license";
142
235
  const data = { client_id: clientId };
143
236
 
144
237
  try {
@@ -171,9 +264,60 @@ export const checkFaceComparisonFeatureLicense = async (clientId) => {
171
264
  }
172
265
  }
173
266
 
174
- export const checkLicense = async (clientId, macAddress) => {
175
- const url = licenseServerAddress + "/check_license";
176
- const data = { client_id: clientId, mac_address: macAddress };
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
+
317
+ const url = getLicenseServerAddress() + "/check_license";
318
+ const data = { client_id: clientId };
319
+ if (macAddress) data.mac_address = macAddress;
320
+ if (serialNumber) data.serial_number = serialNumber;
177
321
 
178
322
  try {
179
323
  const response = await fetch(url, {
@@ -207,7 +351,7 @@ export const checkLicense = async (clientId, macAddress) => {
207
351
  * @returns {Promise<boolean>} True if autoDmv is enabled
208
352
  */
209
353
  export const checkAutoDmvEnabled = async (clientId) => {
210
- const url = licenseServerAddress + "/check_feature_license";
354
+ const url = getLicenseServerAddress() + "/check_feature_license";
211
355
  const data = { client_id: clientId };
212
356
 
213
357
  try {
@@ -232,7 +376,7 @@ export const checkAutoDmvEnabled = async (clientId) => {
232
376
  }
233
377
  };
234
378
  export const getBarcodeLicenseKey = async () => {
235
- const url = licenseServerAddress + "/get_barcode_license_key";
379
+ const url = getLicenseServerAddress() + "/get_barcode_license_key";
236
380
 
237
381
  try {
238
382
  const response = await fetch(url, {
@@ -253,45 +397,148 @@ export const getBarcodeLicenseKey = async () => {
253
397
  }
254
398
  };
255
399
 
256
- // Helper function for making POST requests to the scanner API
257
- const postToScannerApi = async (
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 (
258
439
  scannerAddress,
259
440
  commandObj,
260
- timeout = 10000
441
+ timeout = 10000,
442
+ externalSignal,
261
443
  ) => {
262
- // Ensure the scanner address has a protocol (http:// or https://)
263
- let formattedAddress = scannerAddress;
264
- if (!scannerAddress.startsWith('http://') && !scannerAddress.startsWith('https://')) {
265
- formattedAddress = `http://${scannerAddress}`;
266
- }
267
-
268
- let apiAddress = `${formattedAddress}/securelink`;
444
+ const attemptedUrl = buildSecurelinkUrl(scannerAddress);
269
445
 
270
446
  const controller = new AbortController();
271
- const signal = controller.signal;
272
-
273
- const fetchTimeout = setTimeout(() => {
274
- controller.abort();
275
- }, timeout);
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
+ }
276
460
 
277
461
  try {
278
- const response = await fetch(apiAddress, {
462
+ const response = await fetch(attemptedUrl, {
279
463
  method: "POST",
280
464
  headers: { "Content-Type": "application/json" },
281
465
  body: JSON.stringify(commandObj),
282
- signal: signal,
466
+ signal: controller.signal,
283
467
  });
284
468
 
285
- clearTimeout(fetchTimeout);
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
+ }
286
484
 
287
- if (!response.ok) throw new Error(response.statusText);
288
- return response.json();
485
+ return {
486
+ threw: false,
487
+ ok: response.ok,
488
+ status: response.status,
489
+ statusText: response.statusText,
490
+ body,
491
+ attemptedUrl,
492
+ };
289
493
  } catch (error) {
290
- if (error.name === "AbortError") {
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") {
291
519
  throw new Error("Request timed out");
292
520
  }
293
- 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");
294
539
  }
540
+
541
+ return result.body;
295
542
  };
296
543
 
297
544
  export const getNetworkInfo = (scannerAddress, commandObj) =>
@@ -318,40 +565,239 @@ export const stopScanning = (scannerAddress, commandObj) =>
318
565
  export const closeConnection = (scannerAddress, commandObj) =>
319
566
  postToScannerApi(scannerAddress, commandObj);
320
567
 
321
- // Check if scanner is present by attempting to connect and disconnect
322
- export const checkScannerPresent = async (scannerAddress) => {
323
- try {
324
- // Generate a unique message ID for this check
325
- const messageId = new Date().toISOString();
326
-
327
- // Attempt to establish connection
328
- const connectCommand = {
329
- Command: "Connect",
330
- MessageId: messageId,
331
- Exclusive: "false", // Use non-exclusive mode for just checking
332
- IdleTimeout: "5",
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,
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
+ };
642
+
643
+ const probe = await postToScannerApiDetailed(
644
+ scannerAddress,
645
+ connectCommand,
646
+ 5000,
647
+ signal,
648
+ );
649
+
650
+ if (probe.threw) {
651
+ return {
652
+ ok: false,
653
+ error: classifyFetchError(
654
+ probe.error,
655
+ attemptedUrl,
656
+ Boolean(signal?.aborted),
657
+ ),
333
658
  };
659
+ }
334
660
 
335
- const connectResponse = await postToScannerApi(scannerAddress, connectCommand, 5000);
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
+ },
671
+ };
672
+ }
336
673
 
337
- // If connection failed or no session ID, scanner not present
338
- if (!connectResponse || !connectResponse.SessionId) {
339
- return false;
340
- }
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
+ }
341
686
 
342
- // Got a session ID, now disconnect
343
- const disconnectCommand = {
344
- Command: "Disconnect",
345
- MessageId: new Date().toISOString(),
346
- SessionId: connectResponse.SessionId,
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
+ },
347
708
  };
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;
736
+ };
737
+
738
+ /**
739
+ * Create a verification session for mobile ID verification
740
+ *
741
+ * POST /sessions
742
+ * Returns: session_id, session_url (for QR code), websocket_url (for real-time updates)
743
+ *
744
+ * @param {string} userId - User ID for the verification session
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.
748
+ * @returns {Promise<Object>} Session data with session_id, session_url, and websocket_url
749
+ */
750
+ export const createVerificationSession = async (userId, clientId, origin) => {
751
+ const url = getMobileVerificationServerAddress() + "/sessions";
752
+ const data = { user_id: userId, client_id: clientId };
753
+ if (origin) {
754
+ data.origin = origin;
755
+ }
756
+
757
+ try {
758
+ const response = await fetch(url, {
759
+ method: "POST",
760
+ headers: {
761
+ "Content-Type": "application/json",
762
+ },
763
+ body: JSON.stringify(data),
764
+ });
348
765
 
349
- await postToScannerApi(scannerAddress, disconnectCommand, 5000);
766
+ if (response.ok) {
767
+ const responseData = await response.json();
768
+ return responseData;
769
+ } else {
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;
775
+ try {
776
+ errorData = JSON.parse(errorText);
777
+ } catch {
778
+ // Non-JSON body (e.g. plain text 502 from API Gateway).
779
+ }
350
780
 
351
- // Successfully connected and disconnected
352
- return true;
781
+ if (errorData) {
782
+ console.error("Verification session creation failed:", errorData);
783
+ return {
784
+ status: 'error',
785
+ message: errorData.message || 'Failed to create verification session',
786
+ error: errorData,
787
+ };
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
+ };
795
+ }
353
796
  } catch (error) {
354
- // Any error means scanner is not reachable
355
- return false;
797
+ console.error("There was a problem with the verification session request:", error);
798
+ return {
799
+ status: 'error',
800
+ message: error.message || 'Unable to connect to verification service.'
801
+ };
356
802
  }
357
803
  };