verant_id_cloud_scan 1.4.5-beta.1 → 1.4.5-beta.11

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/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,26 @@ 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
+ // Pass exactly one of macAddress / serialNumber. The license server rejects
268
+ // requests that supply both or neither; we also fail fast here so callers
269
+ // don't have to wait for a 400 to learn they passed a bad identifier.
270
+ export const checkLicense = async (clientId, identifier = {}) => {
271
+ if (identifier === null || typeof identifier !== "object") {
272
+ throw new TypeError(
273
+ `checkLicense: second argument must be an object like { macAddress } or { serialNumber }, got ${typeof identifier}`,
274
+ );
275
+ }
276
+ const { macAddress, serialNumber } = identifier;
277
+ if (Boolean(macAddress) === Boolean(serialNumber)) {
278
+ throw new Error(
279
+ "checkLicense: pass exactly one of macAddress / serialNumber",
280
+ );
281
+ }
282
+
283
+ const url = getLicenseServerAddress() + "/check_license";
284
+ const data = { client_id: clientId };
285
+ if (macAddress) data.mac_address = macAddress;
286
+ if (serialNumber) data.serial_number = serialNumber;
177
287
 
178
288
  try {
179
289
  const response = await fetch(url, {
@@ -207,7 +317,7 @@ export const checkLicense = async (clientId, macAddress) => {
207
317
  * @returns {Promise<boolean>} True if autoDmv is enabled
208
318
  */
209
319
  export const checkAutoDmvEnabled = async (clientId) => {
210
- const url = licenseServerAddress + "/check_feature_license";
320
+ const url = getLicenseServerAddress() + "/check_feature_license";
211
321
  const data = { client_id: clientId };
212
322
 
213
323
  try {
@@ -232,7 +342,7 @@ export const checkAutoDmvEnabled = async (clientId) => {
232
342
  }
233
343
  };
234
344
  export const getBarcodeLicenseKey = async () => {
235
- const url = licenseServerAddress + "/get_barcode_license_key";
345
+ const url = getLicenseServerAddress() + "/get_barcode_license_key";
236
346
 
237
347
  try {
238
348
  const response = await fetch(url, {
@@ -253,45 +363,148 @@ export const getBarcodeLicenseKey = async () => {
253
363
  }
254
364
  };
255
365
 
256
- // Helper function for making POST requests to the scanner API
257
- const postToScannerApi = async (
366
+ /**
367
+ * Build the /securelink URL for the given scanner address.
368
+ *
369
+ * Mirrors the SDK's historical behaviour: if the caller didn't include a
370
+ * scheme, default to http://. The scheme test is case-insensitive per
371
+ * RFC 3986 (VNT-1522) — `HTTP://`, `HTTPS://`, and mixed-case forms are
372
+ * recognised so the SDK doesn't double-prefix them with another `http://`.
373
+ */
374
+ export const buildSecurelinkUrl = (scannerAddress) => {
375
+ const hasScheme = /^https?:\/\//i.test(scannerAddress);
376
+ const withScheme = hasScheme ? scannerAddress : `http://${scannerAddress}`;
377
+ return `${withScheme.replace(/\/+$/, "")}/securelink`;
378
+ };
379
+
380
+ /**
381
+ * Lower-level POST that returns a structured result instead of throwing.
382
+ * Used by `checkScannerPresentDetailed` (VNT-1522) so the caller can
383
+ * distinguish network failures, HTTP errors, parse failures, and scanner
384
+ * application-layer errors. Existing callers go through `postToScannerApi`
385
+ * which preserves the throw-on-error contract.
386
+ *
387
+ * Supports an external `AbortSignal` so host apps can cancel an in-flight
388
+ * probe on retry / unmount without waiting for the internal timeout.
389
+ *
390
+ * @param {string} scannerAddress
391
+ * @param {object} commandObj
392
+ * @param {number} [timeout=10000] timeout in ms before the internal AbortController aborts
393
+ * @param {AbortSignal} [externalSignal] optional external signal; when aborted, the request is cancelled
394
+ * @returns {Promise<{
395
+ * threw: boolean,
396
+ * error?: Error,
397
+ * ok?: boolean,
398
+ * status?: number,
399
+ * statusText?: string,
400
+ * body?: unknown,
401
+ * attemptedUrl: string,
402
+ * }>}
403
+ */
404
+ const postToScannerApiDetailed = async (
258
405
  scannerAddress,
259
406
  commandObj,
260
- timeout = 10000
407
+ timeout = 10000,
408
+ externalSignal,
261
409
  ) => {
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`;
410
+ const attemptedUrl = buildSecurelinkUrl(scannerAddress);
269
411
 
270
412
  const controller = new AbortController();
271
- const signal = controller.signal;
272
-
273
- const fetchTimeout = setTimeout(() => {
274
- controller.abort();
275
- }, timeout);
413
+ const fetchTimeout = setTimeout(() => controller.abort(), timeout);
414
+
415
+ // Forward an external abort signal to our controller so the host app's
416
+ // cancellation (retry, unmount) propagates to the in-flight fetch.
417
+ let externalAbortHandler;
418
+ if (externalSignal) {
419
+ if (externalSignal.aborted) {
420
+ controller.abort();
421
+ } else {
422
+ externalAbortHandler = () => controller.abort();
423
+ externalSignal.addEventListener("abort", externalAbortHandler, { once: true });
424
+ }
425
+ }
276
426
 
277
427
  try {
278
- const response = await fetch(apiAddress, {
428
+ const response = await fetch(attemptedUrl, {
279
429
  method: "POST",
280
430
  headers: { "Content-Type": "application/json" },
281
431
  body: JSON.stringify(commandObj),
282
- signal: signal,
432
+ signal: controller.signal,
283
433
  });
284
434
 
285
- clearTimeout(fetchTimeout);
435
+ // Read the body once as text, then try to JSON-parse — keeps a malformed
436
+ // body from making us discard otherwise-useful status info.
437
+ let body = null;
438
+ try {
439
+ const text = await response.text();
440
+ if (text) {
441
+ try {
442
+ body = JSON.parse(text);
443
+ } catch {
444
+ body = text; // not JSON, but still meaningful for diagnostics
445
+ }
446
+ }
447
+ } catch {
448
+ // body read failed — leave body=null, caller can decide
449
+ }
286
450
 
287
- if (!response.ok) throw new Error(response.statusText);
288
- return response.json();
451
+ return {
452
+ threw: false,
453
+ ok: response.ok,
454
+ status: response.status,
455
+ statusText: response.statusText,
456
+ body,
457
+ attemptedUrl,
458
+ };
289
459
  } catch (error) {
290
- if (error.name === "AbortError") {
460
+ return { threw: true, error, attemptedUrl };
461
+ } finally {
462
+ clearTimeout(fetchTimeout);
463
+ if (externalSignal && externalAbortHandler) {
464
+ externalSignal.removeEventListener("abort", externalAbortHandler);
465
+ }
466
+ }
467
+ };
468
+
469
+ // Helper function for making POST requests to the scanner API. Thin wrapper
470
+ // around `postToScannerApiDetailed` that preserves the historical contract
471
+ // (throws on timeout / network / non-2xx; returns parsed JSON on success).
472
+ const postToScannerApi = async (
473
+ scannerAddress,
474
+ commandObj,
475
+ timeout = 10000
476
+ ) => {
477
+ const result = await postToScannerApiDetailed(
478
+ scannerAddress,
479
+ commandObj,
480
+ timeout,
481
+ );
482
+
483
+ if (result.threw) {
484
+ if (result.error?.name === "AbortError") {
291
485
  throw new Error("Request timed out");
292
486
  }
293
- throw error;
487
+ throw result.error;
488
+ }
489
+
490
+ if (!result.ok) {
491
+ throw new Error(result.statusText || `HTTP ${result.status}`);
492
+ }
493
+
494
+ // Match the original `response.json()` contract: throw if the 2xx body
495
+ // wasn't a parseable JSON object. The detailed wrapper deliberately
496
+ // preserves non-JSON bodies as strings (or `null` for empty) so error
497
+ // surfaces like `checkScannerPresentDetailed` can use them — but every
498
+ // existing caller of `postToScannerApi` (`getNetworkInfo`,
499
+ // `establishConnection`, `getParams`, `resetParams`, `setParameters`,
500
+ // `scanDocument`, …) reads object properties off the return value, so
501
+ // returning a string or null here would silently produce `undefined`
502
+ // downstream instead of failing loudly.
503
+ if (result.body === null || typeof result.body !== "object") {
504
+ throw new SyntaxError("Scanner response was not a JSON object");
294
505
  }
506
+
507
+ return result.body;
295
508
  };
296
509
 
297
510
  export const getNetworkInfo = (scannerAddress, commandObj) =>
@@ -318,40 +531,239 @@ export const stopScanning = (scannerAddress, commandObj) =>
318
531
  export const closeConnection = (scannerAddress, commandObj) =>
319
532
  postToScannerApi(scannerAddress, commandObj);
320
533
 
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",
534
+ /**
535
+ * Classify a thrown fetch error into one of the documented codes (VNT-1522).
536
+ *
537
+ * `externallyAborted` distinguishes a host-app cancellation (retry, unmount)
538
+ * from an internal timeout — both surface as `AbortError` from fetch, but
539
+ * callers branching on `code` for UI copy (e.g. "scanner timed out") should
540
+ * not see the timeout label when the user themselves cancelled.
541
+ *
542
+ * @param {unknown} error
543
+ * @param {string} attemptedUrl
544
+ * @param {boolean} [externallyAborted=false]
545
+ * @returns {{ code: string, name: string, message: string, attemptedUrl: string }}
546
+ */
547
+ const classifyFetchError = (error, attemptedUrl, externallyAborted = false) => {
548
+ if (error?.name === "AbortError") {
549
+ return {
550
+ code: externallyAborted ? "ABORTED" : "TIMEOUT",
551
+ name: "AbortError",
552
+ message: externallyAborted ? "Request aborted" : "Request timed out",
553
+ attemptedUrl,
554
+ };
555
+ }
556
+ if (error instanceof TypeError) {
557
+ // Browser fetch surfaces network failures (DNS, connection refused,
558
+ // mixed-content blocks, CORS preflight rejection) as `TypeError`.
559
+ return {
560
+ code: "NETWORK_ERROR",
561
+ name: "TypeError",
562
+ message: error.message || "Network error",
563
+ attemptedUrl,
333
564
  };
565
+ }
566
+ return {
567
+ code: "NETWORK_ERROR",
568
+ name: error?.name || "Unknown",
569
+ message: error?.message || String(error),
570
+ attemptedUrl,
571
+ };
572
+ };
334
573
 
335
- const connectResponse = await postToScannerApi(scannerAddress, connectCommand, 5000);
574
+ /**
575
+ * Structured version of `checkScannerPresent` (VNT-1522).
576
+ *
577
+ * Returns `{ ok: true }` on success or `{ ok: false, error }` with a tagged
578
+ * `error.code` so host apps can distinguish:
579
+ * - `TIMEOUT` — internal 5s timeout fired
580
+ * - `ABORTED` — host app cancelled via the external `AbortSignal`
581
+ * - `NETWORK_ERROR` — fetch threw (DNS, refused, mixed content, CORS, etc.)
582
+ * - `HTTP_ERROR` — server replied non-2xx
583
+ * - `INVALID_RESPONSE` — 2xx but the body wasn't JSON
584
+ * - `SCANNER_ERROR` — 2xx JSON but no `SessionId` (the scanner returned
585
+ * an application-layer error in the body, e.g. an HTTPS-only scanner
586
+ * rejecting an HTTP probe with `{HttpRC:"403", ExtInformation:"..."}`)
587
+ *
588
+ * The error object also carries `attemptedUrl`, `status` (when a response
589
+ * was received), and the raw `scannerResponse` body for SCANNER_ERROR so
590
+ * callers can surface vendor-specific fields like `ExtInformation`.
591
+ *
592
+ * @param {string} scannerAddress
593
+ * @param {{ signal?: AbortSignal }} [options]
594
+ */
595
+ export const checkScannerPresentDetailed = async (
596
+ scannerAddress,
597
+ options = {},
598
+ ) => {
599
+ const { signal } = options;
600
+ const attemptedUrl = buildSecurelinkUrl(scannerAddress);
601
+
602
+ const connectCommand = {
603
+ Command: "Connect",
604
+ MessageId: new Date().toISOString(),
605
+ Exclusive: "false", // non-exclusive — we're only probing
606
+ IdleTimeout: "5",
607
+ };
336
608
 
337
- // If connection failed or no session ID, scanner not present
338
- if (!connectResponse || !connectResponse.SessionId) {
339
- return false;
340
- }
609
+ const probe = await postToScannerApiDetailed(
610
+ scannerAddress,
611
+ connectCommand,
612
+ 5000,
613
+ signal,
614
+ );
615
+
616
+ if (probe.threw) {
617
+ return {
618
+ ok: false,
619
+ error: classifyFetchError(
620
+ probe.error,
621
+ attemptedUrl,
622
+ Boolean(signal?.aborted),
623
+ ),
624
+ };
625
+ }
341
626
 
342
- // Got a session ID, now disconnect
343
- const disconnectCommand = {
344
- Command: "Disconnect",
345
- MessageId: new Date().toISOString(),
346
- SessionId: connectResponse.SessionId,
627
+ if (!probe.ok) {
628
+ return {
629
+ ok: false,
630
+ error: {
631
+ code: "HTTP_ERROR",
632
+ name: "HTTPError",
633
+ message: probe.statusText || `HTTP ${probe.status}`,
634
+ status: probe.status,
635
+ attemptedUrl,
636
+ },
347
637
  };
638
+ }
348
639
 
349
- await postToScannerApi(scannerAddress, disconnectCommand, 5000);
640
+ if (!probe.body || typeof probe.body !== "object") {
641
+ return {
642
+ ok: false,
643
+ error: {
644
+ code: "INVALID_RESPONSE",
645
+ name: "InvalidResponse",
646
+ message: "Scanner returned a non-JSON or empty body",
647
+ status: probe.status,
648
+ attemptedUrl,
649
+ },
650
+ };
651
+ }
652
+
653
+ if (!probe.body.SessionId) {
654
+ // App-layer scanner error — common when an HTTPS-only scanner is hit
655
+ // over HTTP. The body usually contains vendor-specific fields like
656
+ // `ExtInformation` ("Scanner not configured for HTTP") that we forward
657
+ // so callers can present an actionable message.
658
+ return {
659
+ ok: false,
660
+ error: {
661
+ code: "SCANNER_ERROR",
662
+ name: "ScannerError",
663
+ message:
664
+ probe.body.ExtInformation ||
665
+ probe.body.ErrorInformation ||
666
+ "Scanner returned no SessionId",
667
+ status:
668
+ typeof probe.body.HttpRC === "string"
669
+ ? parseInt(probe.body.HttpRC, 10) || probe.status
670
+ : probe.status,
671
+ attemptedUrl,
672
+ scannerResponse: probe.body,
673
+ },
674
+ };
675
+ }
676
+
677
+ // Got a SessionId — clean up by disconnecting. We don't surface a
678
+ // disconnect failure: from the host app's perspective the scanner IS
679
+ // present and the leaked session will time out server-side per the
680
+ // scanner's IdleTimeout.
681
+ const disconnectCommand = {
682
+ Command: "Disconnect",
683
+ MessageId: new Date().toISOString(),
684
+ SessionId: probe.body.SessionId,
685
+ };
686
+ await postToScannerApiDetailed(
687
+ scannerAddress,
688
+ disconnectCommand,
689
+ 5000,
690
+ signal,
691
+ );
692
+
693
+ return { ok: true };
694
+ };
695
+
696
+ // Boolean form of scanner presence check. Thin wrapper around
697
+ // `checkScannerPresentDetailed` (VNT-1522) — preserved for existing
698
+ // callers; new code should prefer the detailed version.
699
+ export const checkScannerPresent = async (scannerAddress) => {
700
+ const result = await checkScannerPresentDetailed(scannerAddress);
701
+ return result.ok;
702
+ };
703
+
704
+ /**
705
+ * Create a verification session for mobile ID verification
706
+ *
707
+ * POST /sessions
708
+ * Returns: session_id, session_url (for QR code), websocket_url (for real-time updates)
709
+ *
710
+ * @param {string} userId - User ID for the verification session
711
+ * @param {string} clientId - Client ID for licensing and tracking
712
+ * @param {string} [origin] - Holder page origin; binds the server's
713
+ * SessionTranscript to the wallet's view (HPKE info). Required on non-default hosts.
714
+ * @returns {Promise<Object>} Session data with session_id, session_url, and websocket_url
715
+ */
716
+ export const createVerificationSession = async (userId, clientId, origin) => {
717
+ const url = getMobileVerificationServerAddress() + "/sessions";
718
+ const data = { user_id: userId, client_id: clientId };
719
+ if (origin) {
720
+ data.origin = origin;
721
+ }
350
722
 
351
- // Successfully connected and disconnected
352
- return true;
723
+ try {
724
+ const response = await fetch(url, {
725
+ method: "POST",
726
+ headers: {
727
+ "Content-Type": "application/json",
728
+ },
729
+ body: JSON.stringify(data),
730
+ });
731
+
732
+ if (response.ok) {
733
+ const responseData = await response.json();
734
+ return responseData;
735
+ } else {
736
+ // Read the body once as text — fetch consumes the stream on first read,
737
+ // so a `response.json()` then `response.text()` fallback would throw
738
+ // `TypeError: body stream already read` and mask the original error.
739
+ const errorText = await response.text();
740
+ let errorData = null;
741
+ try {
742
+ errorData = JSON.parse(errorText);
743
+ } catch {
744
+ // Non-JSON body (e.g. plain text 502 from API Gateway).
745
+ }
746
+
747
+ if (errorData) {
748
+ console.error("Verification session creation failed:", errorData);
749
+ return {
750
+ status: 'error',
751
+ message: errorData.message || 'Failed to create verification session',
752
+ error: errorData,
753
+ };
754
+ }
755
+ console.error("Verification session creation failed:", errorText);
756
+ return {
757
+ status: 'error',
758
+ message: 'Failed to create verification session',
759
+ raw_error: errorText,
760
+ };
761
+ }
353
762
  } catch (error) {
354
- // Any error means scanner is not reachable
355
- return false;
763
+ console.error("There was a problem with the verification session request:", error);
764
+ return {
765
+ status: 'error',
766
+ message: error.message || 'Unable to connect to verification service.'
767
+ };
356
768
  }
357
769
  };