verant_id_cloud_scan 1.4.5-beta.10 → 1.4.5-beta.13
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 +344 -81
- package/Api.test.js +403 -0
- package/CHANGELOG.md +66 -0
- package/CloudScan.js +5 -2
- package/{FaceComparison.js → FaceApi.js} +40 -1
- package/FieldMapping.js +4 -2
- package/LocalScannerApi.js +47 -32
- package/ScannerApi.js +34 -1
- package/VerificationModal.js +4 -0
- package/index.d.ts +97 -8
- package/package.json +2 -2
package/Api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { compressAndCompareImages } from "./
|
|
1
|
+
import { compressAndCompareImages } from "./FaceApi.js";
|
|
2
2
|
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";
|
|
@@ -62,47 +62,61 @@ export function setEnvironment(environment) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
|
-
*
|
|
65
|
+
* Per-service host by environment.
|
|
66
|
+
*/
|
|
67
|
+
const HOSTS = {
|
|
68
|
+
staging: {
|
|
69
|
+
license: "https://lic-staging.verantid.com/api/v1",
|
|
70
|
+
dmv: "https://dmv-check-server-staging-fcaab48bec21.herokuapp.com",
|
|
71
|
+
face: "https://staging.face.verantid.bluerocket.us",
|
|
72
|
+
mobile: "https://staging.mdl.verantid.bluerocket.us",
|
|
73
|
+
},
|
|
74
|
+
production: {
|
|
75
|
+
license: "https://lic.verantid.com/api/v1",
|
|
76
|
+
dmv: "https://dmv.verantid.com",
|
|
77
|
+
face: "https://faceid.verantid.com",
|
|
78
|
+
mobile: "https://mdl.verantid.bluerocket.us",
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolve a service's host for the current environment.
|
|
84
|
+
* @param {'license' | 'dmv' | 'face' | 'mobile'} service
|
|
66
85
|
* @returns {string}
|
|
67
86
|
*/
|
|
87
|
+
function host(service) {
|
|
88
|
+
return HOSTS[getEnvironment()][service];
|
|
89
|
+
}
|
|
90
|
+
|
|
68
91
|
function getLicenseServerAddress() {
|
|
69
|
-
|
|
70
|
-
return env === 'staging'
|
|
71
|
-
? "https://lic-staging.verantid.com/api/v1"
|
|
72
|
-
: "https://lic.verantid.com/api/v1";
|
|
92
|
+
return host("license");
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
96
|
+
* Still on /api/v1 (SOAP transport), which does NOT carry
|
|
97
|
+
* the personDigitalImage match. The field already forwards in the driver object,
|
|
98
|
+
* but the "License Portrait" match indicator will only round-trip once this path
|
|
99
|
+
* is switched to /api/v2/dmv_check (REST 3.0). That endpoint is request/response
|
|
100
|
+
* compatible — change the path below — but it must NOT be flipped until VNT-1463
|
|
101
|
+
* is deployed to these hosts, or all DMV checks will 404.
|
|
78
102
|
*/
|
|
79
103
|
function getDmvServerAddress() {
|
|
80
|
-
|
|
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";
|
|
104
|
+
return `${host("dmv")}/api/v1/dmv_check`;
|
|
84
105
|
}
|
|
85
106
|
|
|
86
|
-
/**
|
|
87
|
-
* Get Face Comparison server address based on current environment
|
|
88
|
-
* @returns {string}
|
|
89
|
-
*/
|
|
90
107
|
export function getFaceComparisonServerAddress() {
|
|
91
|
-
|
|
92
|
-
return env === 'staging'
|
|
93
|
-
? "https://staging.face.verantid.bluerocket.us/compare_id_and_face"
|
|
94
|
-
: "https://faceid.verantid.com/compare_id_and_face";
|
|
108
|
+
return `${host("face")}/compare_id_and_face`;
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
/**
|
|
98
|
-
*
|
|
99
|
-
* @returns {string}
|
|
112
|
+
* License Portrait extraction
|
|
100
113
|
*/
|
|
114
|
+
export function getExtractPortraitServerAddress() {
|
|
115
|
+
return `${host("face")}/extract_portrait`;
|
|
116
|
+
}
|
|
117
|
+
|
|
101
118
|
function getMobileVerificationServerAddress() {
|
|
102
|
-
|
|
103
|
-
return env === 'staging'
|
|
104
|
-
? "https://staging.mdl.verantid.bluerocket.us"
|
|
105
|
-
: "https://mdl.verantid.bluerocket.us";
|
|
119
|
+
return host("mobile");
|
|
106
120
|
}
|
|
107
121
|
|
|
108
122
|
export const dmvCheck = async (clientId, scannerType, licenseData) => {
|
|
@@ -264,9 +278,26 @@ export const checkFaceComparisonFeatureLicense = async (clientId) => {
|
|
|
264
278
|
}
|
|
265
279
|
}
|
|
266
280
|
|
|
267
|
-
|
|
281
|
+
// Pass exactly one of macAddress / serialNumber. The license server rejects
|
|
282
|
+
// requests that supply both or neither; we also fail fast here so callers
|
|
283
|
+
// don't have to wait for a 400 to learn they passed a bad identifier.
|
|
284
|
+
export const checkLicense = async (clientId, identifier = {}) => {
|
|
285
|
+
if (identifier === null || typeof identifier !== "object") {
|
|
286
|
+
throw new TypeError(
|
|
287
|
+
`checkLicense: second argument must be an object like { macAddress } or { serialNumber }, got ${typeof identifier}`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
const { macAddress, serialNumber } = identifier;
|
|
291
|
+
if (Boolean(macAddress) === Boolean(serialNumber)) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
"checkLicense: pass exactly one of macAddress / serialNumber",
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
268
297
|
const url = getLicenseServerAddress() + "/check_license";
|
|
269
|
-
const data = { client_id: clientId
|
|
298
|
+
const data = { client_id: clientId };
|
|
299
|
+
if (macAddress) data.mac_address = macAddress;
|
|
300
|
+
if (serialNumber) data.serial_number = serialNumber;
|
|
270
301
|
|
|
271
302
|
try {
|
|
272
303
|
const response = await fetch(url, {
|
|
@@ -346,48 +377,148 @@ export const getBarcodeLicenseKey = async () => {
|
|
|
346
377
|
}
|
|
347
378
|
};
|
|
348
379
|
|
|
349
|
-
|
|
350
|
-
|
|
380
|
+
/**
|
|
381
|
+
* Build the /securelink URL for the given scanner address.
|
|
382
|
+
*
|
|
383
|
+
* Mirrors the SDK's historical behaviour: if the caller didn't include a
|
|
384
|
+
* scheme, default to http://. The scheme test is case-insensitive per
|
|
385
|
+
* RFC 3986 (VNT-1522) — `HTTP://`, `HTTPS://`, and mixed-case forms are
|
|
386
|
+
* recognised so the SDK doesn't double-prefix them with another `http://`.
|
|
387
|
+
*/
|
|
388
|
+
export const buildSecurelinkUrl = (scannerAddress) => {
|
|
389
|
+
const hasScheme = /^https?:\/\//i.test(scannerAddress);
|
|
390
|
+
const withScheme = hasScheme ? scannerAddress : `http://${scannerAddress}`;
|
|
391
|
+
return `${withScheme.replace(/\/+$/, "")}/securelink`;
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Lower-level POST that returns a structured result instead of throwing.
|
|
396
|
+
* Used by `checkScannerPresentDetailed` (VNT-1522) so the caller can
|
|
397
|
+
* distinguish network failures, HTTP errors, parse failures, and scanner
|
|
398
|
+
* application-layer errors. Existing callers go through `postToScannerApi`
|
|
399
|
+
* which preserves the throw-on-error contract.
|
|
400
|
+
*
|
|
401
|
+
* Supports an external `AbortSignal` so host apps can cancel an in-flight
|
|
402
|
+
* probe on retry / unmount without waiting for the internal timeout.
|
|
403
|
+
*
|
|
404
|
+
* @param {string} scannerAddress
|
|
405
|
+
* @param {object} commandObj
|
|
406
|
+
* @param {number} [timeout=10000] timeout in ms before the internal AbortController aborts
|
|
407
|
+
* @param {AbortSignal} [externalSignal] optional external signal; when aborted, the request is cancelled
|
|
408
|
+
* @returns {Promise<{
|
|
409
|
+
* threw: boolean,
|
|
410
|
+
* error?: Error,
|
|
411
|
+
* ok?: boolean,
|
|
412
|
+
* status?: number,
|
|
413
|
+
* statusText?: string,
|
|
414
|
+
* body?: unknown,
|
|
415
|
+
* attemptedUrl: string,
|
|
416
|
+
* }>}
|
|
417
|
+
*/
|
|
418
|
+
const postToScannerApiDetailed = async (
|
|
351
419
|
scannerAddress,
|
|
352
420
|
commandObj,
|
|
353
|
-
timeout = 10000
|
|
421
|
+
timeout = 10000,
|
|
422
|
+
externalSignal,
|
|
354
423
|
) => {
|
|
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`;
|
|
424
|
+
const attemptedUrl = buildSecurelinkUrl(scannerAddress);
|
|
365
425
|
|
|
366
426
|
const controller = new AbortController();
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
427
|
+
const fetchTimeout = setTimeout(() => controller.abort(), timeout);
|
|
428
|
+
|
|
429
|
+
// Forward an external abort signal to our controller so the host app's
|
|
430
|
+
// cancellation (retry, unmount) propagates to the in-flight fetch.
|
|
431
|
+
let externalAbortHandler;
|
|
432
|
+
if (externalSignal) {
|
|
433
|
+
if (externalSignal.aborted) {
|
|
434
|
+
controller.abort();
|
|
435
|
+
} else {
|
|
436
|
+
externalAbortHandler = () => controller.abort();
|
|
437
|
+
externalSignal.addEventListener("abort", externalAbortHandler, { once: true });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
372
440
|
|
|
373
441
|
try {
|
|
374
|
-
const response = await fetch(
|
|
442
|
+
const response = await fetch(attemptedUrl, {
|
|
375
443
|
method: "POST",
|
|
376
444
|
headers: { "Content-Type": "application/json" },
|
|
377
445
|
body: JSON.stringify(commandObj),
|
|
378
|
-
signal: signal,
|
|
446
|
+
signal: controller.signal,
|
|
379
447
|
});
|
|
380
448
|
|
|
381
|
-
|
|
449
|
+
// Read the body once as text, then try to JSON-parse — keeps a malformed
|
|
450
|
+
// body from making us discard otherwise-useful status info.
|
|
451
|
+
let body = null;
|
|
452
|
+
try {
|
|
453
|
+
const text = await response.text();
|
|
454
|
+
if (text) {
|
|
455
|
+
try {
|
|
456
|
+
body = JSON.parse(text);
|
|
457
|
+
} catch {
|
|
458
|
+
body = text; // not JSON, but still meaningful for diagnostics
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
} catch {
|
|
462
|
+
// body read failed — leave body=null, caller can decide
|
|
463
|
+
}
|
|
382
464
|
|
|
383
|
-
|
|
384
|
-
|
|
465
|
+
return {
|
|
466
|
+
threw: false,
|
|
467
|
+
ok: response.ok,
|
|
468
|
+
status: response.status,
|
|
469
|
+
statusText: response.statusText,
|
|
470
|
+
body,
|
|
471
|
+
attemptedUrl,
|
|
472
|
+
};
|
|
385
473
|
} catch (error) {
|
|
386
|
-
|
|
474
|
+
return { threw: true, error, attemptedUrl };
|
|
475
|
+
} finally {
|
|
476
|
+
clearTimeout(fetchTimeout);
|
|
477
|
+
if (externalSignal && externalAbortHandler) {
|
|
478
|
+
externalSignal.removeEventListener("abort", externalAbortHandler);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
// Helper function for making POST requests to the scanner API. Thin wrapper
|
|
484
|
+
// around `postToScannerApiDetailed` that preserves the historical contract
|
|
485
|
+
// (throws on timeout / network / non-2xx; returns parsed JSON on success).
|
|
486
|
+
const postToScannerApi = async (
|
|
487
|
+
scannerAddress,
|
|
488
|
+
commandObj,
|
|
489
|
+
timeout = 10000
|
|
490
|
+
) => {
|
|
491
|
+
const result = await postToScannerApiDetailed(
|
|
492
|
+
scannerAddress,
|
|
493
|
+
commandObj,
|
|
494
|
+
timeout,
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
if (result.threw) {
|
|
498
|
+
if (result.error?.name === "AbortError") {
|
|
387
499
|
throw new Error("Request timed out");
|
|
388
500
|
}
|
|
389
|
-
throw error;
|
|
501
|
+
throw result.error;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (!result.ok) {
|
|
505
|
+
throw new Error(result.statusText || `HTTP ${result.status}`);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Match the original `response.json()` contract: throw if the 2xx body
|
|
509
|
+
// wasn't a parseable JSON object. The detailed wrapper deliberately
|
|
510
|
+
// preserves non-JSON bodies as strings (or `null` for empty) so error
|
|
511
|
+
// surfaces like `checkScannerPresentDetailed` can use them — but every
|
|
512
|
+
// existing caller of `postToScannerApi` (`getNetworkInfo`,
|
|
513
|
+
// `establishConnection`, `getParams`, `resetParams`, `setParameters`,
|
|
514
|
+
// `scanDocument`, …) reads object properties off the return value, so
|
|
515
|
+
// returning a string or null here would silently produce `undefined`
|
|
516
|
+
// downstream instead of failing loudly.
|
|
517
|
+
if (result.body === null || typeof result.body !== "object") {
|
|
518
|
+
throw new SyntaxError("Scanner response was not a JSON object");
|
|
390
519
|
}
|
|
520
|
+
|
|
521
|
+
return result.body;
|
|
391
522
|
};
|
|
392
523
|
|
|
393
524
|
export const getNetworkInfo = (scannerAddress, commandObj) =>
|
|
@@ -414,42 +545,174 @@ export const stopScanning = (scannerAddress, commandObj) =>
|
|
|
414
545
|
export const closeConnection = (scannerAddress, commandObj) =>
|
|
415
546
|
postToScannerApi(scannerAddress, commandObj);
|
|
416
547
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
548
|
+
/**
|
|
549
|
+
* Classify a thrown fetch error into one of the documented codes (VNT-1522).
|
|
550
|
+
*
|
|
551
|
+
* `externallyAborted` distinguishes a host-app cancellation (retry, unmount)
|
|
552
|
+
* from an internal timeout — both surface as `AbortError` from fetch, but
|
|
553
|
+
* callers branching on `code` for UI copy (e.g. "scanner timed out") should
|
|
554
|
+
* not see the timeout label when the user themselves cancelled.
|
|
555
|
+
*
|
|
556
|
+
* @param {unknown} error
|
|
557
|
+
* @param {string} attemptedUrl
|
|
558
|
+
* @param {boolean} [externallyAborted=false]
|
|
559
|
+
* @returns {{ code: string, name: string, message: string, attemptedUrl: string }}
|
|
560
|
+
*/
|
|
561
|
+
const classifyFetchError = (error, attemptedUrl, externallyAborted = false) => {
|
|
562
|
+
if (error?.name === "AbortError") {
|
|
563
|
+
return {
|
|
564
|
+
code: externallyAborted ? "ABORTED" : "TIMEOUT",
|
|
565
|
+
name: "AbortError",
|
|
566
|
+
message: externallyAborted ? "Request aborted" : "Request timed out",
|
|
567
|
+
attemptedUrl,
|
|
429
568
|
};
|
|
569
|
+
}
|
|
570
|
+
if (error instanceof TypeError) {
|
|
571
|
+
// Browser fetch surfaces network failures (DNS, connection refused,
|
|
572
|
+
// mixed-content blocks, CORS preflight rejection) as `TypeError`.
|
|
573
|
+
return {
|
|
574
|
+
code: "NETWORK_ERROR",
|
|
575
|
+
name: "TypeError",
|
|
576
|
+
message: error.message || "Network error",
|
|
577
|
+
attemptedUrl,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
code: "NETWORK_ERROR",
|
|
582
|
+
name: error?.name || "Unknown",
|
|
583
|
+
message: error?.message || String(error),
|
|
584
|
+
attemptedUrl,
|
|
585
|
+
};
|
|
586
|
+
};
|
|
430
587
|
|
|
431
|
-
|
|
588
|
+
/**
|
|
589
|
+
* Structured version of `checkScannerPresent` (VNT-1522).
|
|
590
|
+
*
|
|
591
|
+
* Returns `{ ok: true }` on success or `{ ok: false, error }` with a tagged
|
|
592
|
+
* `error.code` so host apps can distinguish:
|
|
593
|
+
* - `TIMEOUT` — internal 5s timeout fired
|
|
594
|
+
* - `ABORTED` — host app cancelled via the external `AbortSignal`
|
|
595
|
+
* - `NETWORK_ERROR` — fetch threw (DNS, refused, mixed content, CORS, etc.)
|
|
596
|
+
* - `HTTP_ERROR` — server replied non-2xx
|
|
597
|
+
* - `INVALID_RESPONSE` — 2xx but the body wasn't JSON
|
|
598
|
+
* - `SCANNER_ERROR` — 2xx JSON but no `SessionId` (the scanner returned
|
|
599
|
+
* an application-layer error in the body, e.g. an HTTPS-only scanner
|
|
600
|
+
* rejecting an HTTP probe with `{HttpRC:"403", ExtInformation:"..."}`)
|
|
601
|
+
*
|
|
602
|
+
* The error object also carries `attemptedUrl`, `status` (when a response
|
|
603
|
+
* was received), and the raw `scannerResponse` body for SCANNER_ERROR so
|
|
604
|
+
* callers can surface vendor-specific fields like `ExtInformation`.
|
|
605
|
+
*
|
|
606
|
+
* @param {string} scannerAddress
|
|
607
|
+
* @param {{ signal?: AbortSignal }} [options]
|
|
608
|
+
*/
|
|
609
|
+
export const checkScannerPresentDetailed = async (
|
|
610
|
+
scannerAddress,
|
|
611
|
+
options = {},
|
|
612
|
+
) => {
|
|
613
|
+
const { signal } = options;
|
|
614
|
+
const attemptedUrl = buildSecurelinkUrl(scannerAddress);
|
|
615
|
+
|
|
616
|
+
const connectCommand = {
|
|
617
|
+
Command: "Connect",
|
|
618
|
+
MessageId: new Date().toISOString(),
|
|
619
|
+
Exclusive: "false", // non-exclusive — we're only probing
|
|
620
|
+
IdleTimeout: "5",
|
|
621
|
+
};
|
|
432
622
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
623
|
+
const probe = await postToScannerApiDetailed(
|
|
624
|
+
scannerAddress,
|
|
625
|
+
connectCommand,
|
|
626
|
+
5000,
|
|
627
|
+
signal,
|
|
628
|
+
);
|
|
437
629
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
630
|
+
if (probe.threw) {
|
|
631
|
+
return {
|
|
632
|
+
ok: false,
|
|
633
|
+
error: classifyFetchError(
|
|
634
|
+
probe.error,
|
|
635
|
+
attemptedUrl,
|
|
636
|
+
Boolean(signal?.aborted),
|
|
637
|
+
),
|
|
443
638
|
};
|
|
639
|
+
}
|
|
444
640
|
|
|
445
|
-
|
|
641
|
+
if (!probe.ok) {
|
|
642
|
+
return {
|
|
643
|
+
ok: false,
|
|
644
|
+
error: {
|
|
645
|
+
code: "HTTP_ERROR",
|
|
646
|
+
name: "HTTPError",
|
|
647
|
+
message: probe.statusText || `HTTP ${probe.status}`,
|
|
648
|
+
status: probe.status,
|
|
649
|
+
attemptedUrl,
|
|
650
|
+
},
|
|
651
|
+
};
|
|
652
|
+
}
|
|
446
653
|
|
|
447
|
-
|
|
448
|
-
return
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
654
|
+
if (!probe.body || typeof probe.body !== "object") {
|
|
655
|
+
return {
|
|
656
|
+
ok: false,
|
|
657
|
+
error: {
|
|
658
|
+
code: "INVALID_RESPONSE",
|
|
659
|
+
name: "InvalidResponse",
|
|
660
|
+
message: "Scanner returned a non-JSON or empty body",
|
|
661
|
+
status: probe.status,
|
|
662
|
+
attemptedUrl,
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (!probe.body.SessionId) {
|
|
668
|
+
// App-layer scanner error — common when an HTTPS-only scanner is hit
|
|
669
|
+
// over HTTP. The body usually contains vendor-specific fields like
|
|
670
|
+
// `ExtInformation` ("Scanner not configured for HTTP") that we forward
|
|
671
|
+
// so callers can present an actionable message.
|
|
672
|
+
return {
|
|
673
|
+
ok: false,
|
|
674
|
+
error: {
|
|
675
|
+
code: "SCANNER_ERROR",
|
|
676
|
+
name: "ScannerError",
|
|
677
|
+
message:
|
|
678
|
+
probe.body.ExtInformation ||
|
|
679
|
+
probe.body.ErrorInformation ||
|
|
680
|
+
"Scanner returned no SessionId",
|
|
681
|
+
status:
|
|
682
|
+
typeof probe.body.HttpRC === "string"
|
|
683
|
+
? parseInt(probe.body.HttpRC, 10) || probe.status
|
|
684
|
+
: probe.status,
|
|
685
|
+
attemptedUrl,
|
|
686
|
+
scannerResponse: probe.body,
|
|
687
|
+
},
|
|
688
|
+
};
|
|
452
689
|
}
|
|
690
|
+
|
|
691
|
+
// Got a SessionId — clean up by disconnecting. We don't surface a
|
|
692
|
+
// disconnect failure: from the host app's perspective the scanner IS
|
|
693
|
+
// present and the leaked session will time out server-side per the
|
|
694
|
+
// scanner's IdleTimeout.
|
|
695
|
+
const disconnectCommand = {
|
|
696
|
+
Command: "Disconnect",
|
|
697
|
+
MessageId: new Date().toISOString(),
|
|
698
|
+
SessionId: probe.body.SessionId,
|
|
699
|
+
};
|
|
700
|
+
await postToScannerApiDetailed(
|
|
701
|
+
scannerAddress,
|
|
702
|
+
disconnectCommand,
|
|
703
|
+
5000,
|
|
704
|
+
signal,
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
return { ok: true };
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
// Boolean form of scanner presence check. Thin wrapper around
|
|
711
|
+
// `checkScannerPresentDetailed` (VNT-1522) — preserved for existing
|
|
712
|
+
// callers; new code should prefer the detailed version.
|
|
713
|
+
export const checkScannerPresent = async (scannerAddress) => {
|
|
714
|
+
const result = await checkScannerPresentDetailed(scannerAddress);
|
|
715
|
+
return result.ok;
|
|
453
716
|
};
|
|
454
717
|
|
|
455
718
|
/**
|