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.
- package/.github/workflows/publish_library.yml +9 -1
- package/Api.js +511 -65
- package/Api.test.js +338 -0
- package/CHANGELOG.md +254 -0
- package/CloudScan.js +185 -3
- package/FaceComparison.js +2 -4
- package/LocalApi.js +16 -22
- package/LocalScannerApi.js +47 -32
- package/ScannerApi.js +193 -76
- package/ScannerProfiles.js +132 -0
- package/WebSocketClient.js +281 -0
- package/index.d.ts +175 -2
- package/package.json +12 -3
package/Api.test.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the structured scanner-presence API (VNT-1522).
|
|
3
|
+
*
|
|
4
|
+
* Uses node:test (built-in, no extra deps) — run with `npm test`.
|
|
5
|
+
* `globalThis.fetch` is stubbed per test so we can simulate timeouts,
|
|
6
|
+
* network failures, HTTP errors, and scanner-layer body errors without
|
|
7
|
+
* needing a real local scanner.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
checkScannerPresentDetailed,
|
|
15
|
+
buildSecurelinkUrl,
|
|
16
|
+
getNetworkInfo,
|
|
17
|
+
} from "./Api.js";
|
|
18
|
+
|
|
19
|
+
let originalFetch;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
originalFetch = globalThis.fetch;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
globalThis.fetch = originalFetch;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/** Build a Response-like object the production code will treat as a real Response. */
|
|
30
|
+
function jsonResponse(body, init = {}) {
|
|
31
|
+
return new Response(JSON.stringify(body), {
|
|
32
|
+
status: 200,
|
|
33
|
+
headers: { "Content-Type": "application/json" },
|
|
34
|
+
...init,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("buildSecurelinkUrl — case-insensitive scheme detection (VNT-1522)", () => {
|
|
39
|
+
it("prepends http:// when no scheme is given", () => {
|
|
40
|
+
assert.equal(
|
|
41
|
+
buildSecurelinkUrl("192.168.1.50"),
|
|
42
|
+
"http://192.168.1.50/securelink",
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("preserves a lowercase https:// scheme", () => {
|
|
47
|
+
assert.equal(
|
|
48
|
+
buildSecurelinkUrl("https://192.168.1.50"),
|
|
49
|
+
"https://192.168.1.50/securelink",
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("preserves a lowercase http:// scheme", () => {
|
|
54
|
+
assert.equal(
|
|
55
|
+
buildSecurelinkUrl("http://192.168.1.50"),
|
|
56
|
+
"http://192.168.1.50/securelink",
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("preserves an UPPERCASE HTTPS:// scheme and does not double-prefix", () => {
|
|
61
|
+
// Previously this regressed to `http://HTTPS://...` because the SDK's
|
|
62
|
+
// `startsWith("https://")` check was case-sensitive.
|
|
63
|
+
assert.equal(
|
|
64
|
+
buildSecurelinkUrl("HTTPS://192.168.1.50"),
|
|
65
|
+
"HTTPS://192.168.1.50/securelink",
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("preserves a mixed-case Https:// scheme", () => {
|
|
70
|
+
assert.equal(
|
|
71
|
+
buildSecurelinkUrl("Https://192.168.1.50"),
|
|
72
|
+
"Https://192.168.1.50/securelink",
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("strips trailing slashes so /securelink is appended cleanly", () => {
|
|
77
|
+
assert.equal(
|
|
78
|
+
buildSecurelinkUrl("http://192.168.1.50/"),
|
|
79
|
+
"http://192.168.1.50/securelink",
|
|
80
|
+
);
|
|
81
|
+
assert.equal(
|
|
82
|
+
buildSecurelinkUrl("http://192.168.1.50///"),
|
|
83
|
+
"http://192.168.1.50/securelink",
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("checkScannerPresentDetailed — happy path", () => {
|
|
89
|
+
it("returns ok=true when Connect responds with a SessionId", async () => {
|
|
90
|
+
let connectCalled = false;
|
|
91
|
+
let disconnectCalled = false;
|
|
92
|
+
|
|
93
|
+
globalThis.fetch = async (_url, init) => {
|
|
94
|
+
const body = JSON.parse(init.body);
|
|
95
|
+
if (body.Command === "Connect") {
|
|
96
|
+
connectCalled = true;
|
|
97
|
+
return jsonResponse({ SessionId: "abc-123" });
|
|
98
|
+
}
|
|
99
|
+
if (body.Command === "Disconnect") {
|
|
100
|
+
disconnectCalled = true;
|
|
101
|
+
return jsonResponse({});
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`Unexpected command: ${body.Command}`);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const result = await checkScannerPresentDetailed("192.168.1.50");
|
|
107
|
+
assert.deepEqual(result, { ok: true });
|
|
108
|
+
assert.equal(connectCalled, true, "Connect should have been called");
|
|
109
|
+
assert.equal(disconnectCalled, true, "Disconnect should have been called");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("still returns ok=true when Disconnect fails (scanner is present regardless)", async () => {
|
|
113
|
+
let attempt = 0;
|
|
114
|
+
globalThis.fetch = async (_url, init) => {
|
|
115
|
+
attempt += 1;
|
|
116
|
+
const body = JSON.parse(init.body);
|
|
117
|
+
if (body.Command === "Connect") {
|
|
118
|
+
return jsonResponse({ SessionId: "abc-123" });
|
|
119
|
+
}
|
|
120
|
+
// Disconnect call fails — should NOT degrade the ok=true return,
|
|
121
|
+
// since the host app's question ("is the scanner present?") has
|
|
122
|
+
// already been answered yes.
|
|
123
|
+
throw new TypeError("network error on disconnect");
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const result = await checkScannerPresentDetailed("192.168.1.50");
|
|
127
|
+
assert.equal(result.ok, true);
|
|
128
|
+
assert.equal(attempt, 2, "both Connect and Disconnect should be attempted");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("checkScannerPresentDetailed — error codes", () => {
|
|
133
|
+
it("returns SCANNER_ERROR when 200 OK has no SessionId in body", async () => {
|
|
134
|
+
// Becca's case: HTTPS-only scanner being hit over HTTP returns 200 OK
|
|
135
|
+
// with a body like `{"HttpRC":"403", "ExtInformation":"Scanner not
|
|
136
|
+
// configured for HTTP"}`. The SDK's old boolean form collapsed this
|
|
137
|
+
// to `false`; the detailed form surfaces the vendor message.
|
|
138
|
+
globalThis.fetch = async () =>
|
|
139
|
+
jsonResponse({
|
|
140
|
+
HttpRC: "403",
|
|
141
|
+
Result: "12",
|
|
142
|
+
ErrorInformation: "Device specific error",
|
|
143
|
+
ExtInformation: "Scanner not configured for HTTP",
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const result = await checkScannerPresentDetailed("192.168.1.50");
|
|
147
|
+
assert.equal(result.ok, false);
|
|
148
|
+
assert.equal(result.error.code, "SCANNER_ERROR");
|
|
149
|
+
assert.equal(result.error.message, "Scanner not configured for HTTP");
|
|
150
|
+
assert.equal(result.error.status, 403);
|
|
151
|
+
assert.equal(result.error.attemptedUrl, "http://192.168.1.50/securelink");
|
|
152
|
+
assert.equal(
|
|
153
|
+
result.error.scannerResponse.ExtInformation,
|
|
154
|
+
"Scanner not configured for HTTP",
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("returns HTTP_ERROR when the server replies with a non-2xx status", async () => {
|
|
159
|
+
globalThis.fetch = async () =>
|
|
160
|
+
new Response("Forbidden", { status: 403, statusText: "Forbidden" });
|
|
161
|
+
|
|
162
|
+
const result = await checkScannerPresentDetailed("192.168.1.50");
|
|
163
|
+
assert.equal(result.ok, false);
|
|
164
|
+
assert.equal(result.error.code, "HTTP_ERROR");
|
|
165
|
+
assert.equal(result.error.status, 403);
|
|
166
|
+
assert.equal(result.error.message, "Forbidden");
|
|
167
|
+
assert.equal(result.error.attemptedUrl, "http://192.168.1.50/securelink");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("returns NETWORK_ERROR when fetch throws a TypeError (DNS, CORS, mixed content, etc.)", async () => {
|
|
171
|
+
globalThis.fetch = async () => {
|
|
172
|
+
throw new TypeError("Failed to fetch");
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const result = await checkScannerPresentDetailed("192.168.1.50");
|
|
176
|
+
assert.equal(result.ok, false);
|
|
177
|
+
assert.equal(result.error.code, "NETWORK_ERROR");
|
|
178
|
+
assert.equal(result.error.name, "TypeError");
|
|
179
|
+
assert.match(result.error.message, /Failed to fetch/);
|
|
180
|
+
assert.equal(result.error.attemptedUrl, "http://192.168.1.50/securelink");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("returns ABORTED when an already-aborted external signal is passed", async () => {
|
|
184
|
+
// Real browser fetch rejects synchronously when given an already-
|
|
185
|
+
// aborted signal — mirror that contract in the mock so the test
|
|
186
|
+
// doesn't deadlock if the listener is attached post-abort.
|
|
187
|
+
globalThis.fetch = async (_url, init) => {
|
|
188
|
+
if (init.signal?.aborted) {
|
|
189
|
+
const err = new Error("aborted");
|
|
190
|
+
err.name = "AbortError";
|
|
191
|
+
throw err;
|
|
192
|
+
}
|
|
193
|
+
return new Promise((_resolve, reject) => {
|
|
194
|
+
init.signal?.addEventListener("abort", () => {
|
|
195
|
+
const err = new Error("aborted");
|
|
196
|
+
err.name = "AbortError";
|
|
197
|
+
reject(err);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const controller = new AbortController();
|
|
203
|
+
controller.abort();
|
|
204
|
+
const result = await checkScannerPresentDetailed("192.168.1.50", {
|
|
205
|
+
signal: controller.signal,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
assert.equal(result.ok, false);
|
|
209
|
+
assert.equal(result.error.code, "ABORTED");
|
|
210
|
+
assert.equal(result.error.name, "AbortError");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("returns TIMEOUT when fetch aborts and no external signal was supplied", async () => {
|
|
214
|
+
// Simulates the internal 5s AbortController firing — fetch throws
|
|
215
|
+
// AbortError, no external signal was passed in, so classification
|
|
216
|
+
// falls through to TIMEOUT rather than ABORTED.
|
|
217
|
+
globalThis.fetch = async () => {
|
|
218
|
+
const err = new Error("aborted");
|
|
219
|
+
err.name = "AbortError";
|
|
220
|
+
throw err;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const result = await checkScannerPresentDetailed("192.168.1.50");
|
|
224
|
+
|
|
225
|
+
assert.equal(result.ok, false);
|
|
226
|
+
assert.equal(result.error.code, "TIMEOUT");
|
|
227
|
+
assert.equal(result.error.name, "AbortError");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("returns INVALID_RESPONSE when the body isn't JSON", async () => {
|
|
231
|
+
globalThis.fetch = async () =>
|
|
232
|
+
new Response("<html>not json</html>", {
|
|
233
|
+
status: 200,
|
|
234
|
+
headers: { "Content-Type": "text/html" },
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const result = await checkScannerPresentDetailed("192.168.1.50");
|
|
238
|
+
assert.equal(result.ok, false);
|
|
239
|
+
// The body parses as a non-object string, so the `typeof !== "object"`
|
|
240
|
+
// branch fires before the SessionId check.
|
|
241
|
+
assert.equal(result.error.code, "INVALID_RESPONSE");
|
|
242
|
+
assert.equal(result.error.status, 200);
|
|
243
|
+
assert.equal(result.error.attemptedUrl, "http://192.168.1.50/securelink");
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("checkScannerPresentDetailed — AbortSignal support", () => {
|
|
248
|
+
it("propagates an external abort to the in-flight fetch", async () => {
|
|
249
|
+
let fetchSignal = null;
|
|
250
|
+
globalThis.fetch = async (_url, init) => {
|
|
251
|
+
fetchSignal = init.signal;
|
|
252
|
+
if (init.signal?.aborted) {
|
|
253
|
+
const err = new Error("aborted");
|
|
254
|
+
err.name = "AbortError";
|
|
255
|
+
throw err;
|
|
256
|
+
}
|
|
257
|
+
return new Promise((_resolve, reject) => {
|
|
258
|
+
init.signal.addEventListener("abort", () => {
|
|
259
|
+
const err = new Error("aborted");
|
|
260
|
+
err.name = "AbortError";
|
|
261
|
+
reject(err);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const controller = new AbortController();
|
|
267
|
+
const resultPromise = checkScannerPresentDetailed("192.168.1.50", {
|
|
268
|
+
signal: controller.signal,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Give the fetch microtask a beat to start, then abort externally.
|
|
272
|
+
await Promise.resolve();
|
|
273
|
+
controller.abort();
|
|
274
|
+
|
|
275
|
+
const result = await resultPromise;
|
|
276
|
+
assert.equal(result.ok, false);
|
|
277
|
+
assert.equal(result.error.code, "ABORTED");
|
|
278
|
+
assert.equal(
|
|
279
|
+
fetchSignal.aborted,
|
|
280
|
+
true,
|
|
281
|
+
"the fetch's signal should have been aborted",
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe("postToScannerApi (throw-on-error wrapper) — JSON contract (VNT-1522)", () => {
|
|
287
|
+
// Tested via `getNetworkInfo`, the simplest of the thin wrappers around
|
|
288
|
+
// `postToScannerApi`. Behaviour must match here for every wrapper —
|
|
289
|
+
// they're identical one-liners.
|
|
290
|
+
|
|
291
|
+
it("returns the parsed object on a 2xx JSON response", async () => {
|
|
292
|
+
globalThis.fetch = async () => jsonResponse({ NetworkInfo: { ip: "x" } });
|
|
293
|
+
const result = await getNetworkInfo("192.168.1.50", { Command: "GetNetworkInfo" });
|
|
294
|
+
assert.deepEqual(result, { NetworkInfo: { ip: "x" } });
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("throws on a 2xx response with a non-JSON body (preserves the response.json() contract)", async () => {
|
|
298
|
+
// Pre-VNT-1522 behaviour: the wrapper called `response.json()` which
|
|
299
|
+
// threw `SyntaxError` on invalid JSON. The detailed-form refactor
|
|
300
|
+
// accepted non-JSON bodies (kept them as strings for diagnostics);
|
|
301
|
+
// the wrapper has to re-throw so existing callers — which all read
|
|
302
|
+
// object properties off the return value — fail loudly instead of
|
|
303
|
+
// silently receiving a string.
|
|
304
|
+
globalThis.fetch = async () =>
|
|
305
|
+
new Response("not json", {
|
|
306
|
+
status: 200,
|
|
307
|
+
headers: { "Content-Type": "text/plain" },
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
await assert.rejects(
|
|
311
|
+
getNetworkInfo("192.168.1.50", { Command: "GetNetworkInfo" }),
|
|
312
|
+
/not a JSON object/,
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("throws on a 2xx response with an empty body", async () => {
|
|
317
|
+
globalThis.fetch = async () =>
|
|
318
|
+
new Response("", {
|
|
319
|
+
status: 200,
|
|
320
|
+
headers: { "Content-Type": "application/json" },
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
await assert.rejects(
|
|
324
|
+
getNetworkInfo("192.168.1.50", { Command: "GetNetworkInfo" }),
|
|
325
|
+
/not a JSON object/,
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("throws on non-2xx HTTP responses (unchanged)", async () => {
|
|
330
|
+
globalThis.fetch = async () =>
|
|
331
|
+
new Response("Forbidden", { status: 403, statusText: "Forbidden" });
|
|
332
|
+
|
|
333
|
+
await assert.rejects(
|
|
334
|
+
getNetworkInfo("192.168.1.50", { Command: "GetNetworkInfo" }),
|
|
335
|
+
/Forbidden/,
|
|
336
|
+
);
|
|
337
|
+
});
|
|
338
|
+
});
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.4.6-beta.0] - 2026-07-01
|
|
9
|
+
|
|
10
|
+
> First beta of the `1.4.6` line, cut from the reset (SF-free) `staging`
|
|
11
|
+
> (`1.4.5` is already published as `latest`, so further `1.4.5-beta`s would sort
|
|
12
|
+
> below it). Does **not** include the VNT-1464 / AAMVA 3.0 work
|
|
13
|
+
> (`extractPortraitFromLicense`, `person_digital_image`) — that rejoins on a
|
|
14
|
+
> later beta once AAMVA 3.0 ships.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **`checkDigitalIDFeature(clientId)`** (VNT-1643). Returns `true` only when the
|
|
18
|
+
client has a `mobile_id` feature license that exists and is unexpired, by
|
|
19
|
+
POSTing to the license server's `/mobile_id/check_license` endpoint. Mirrors
|
|
20
|
+
`checkDmvFeature` / `checkFaceCompareFeature` through `Api.js` →
|
|
21
|
+
`ScannerApi.js` → `CloudScan.js`, and is typed in `index.d.ts`. Lets host apps
|
|
22
|
+
gate the "Read Digital ID" capture method on MDL licensing.
|
|
23
|
+
|
|
24
|
+
## [1.4.5-beta.11] - 2026-05-21
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **`scannerPresentDetailed(scannerAddress, options?)`** (VNT-1522). Returns
|
|
28
|
+
a structured `{ ok, error? }` result so host apps can distinguish
|
|
29
|
+
timeouts, host-app cancellations, network errors, HTTP-level errors,
|
|
30
|
+
application-layer scanner errors (e.g. HTTPS-only scanner refusing HTTP
|
|
31
|
+
with `HttpRC:403` / `ExtInformation` in the body), and missing-
|
|
32
|
+
`SessionId` responses. The `error` object carries `code` (`TIMEOUT` |
|
|
33
|
+
`ABORTED` | `NETWORK_ERROR` | `HTTP_ERROR` | `INVALID_RESPONSE` |
|
|
34
|
+
`SCANNER_ERROR`), `name`, `message`, `status`, `attemptedUrl`, and the
|
|
35
|
+
raw `scannerResponse` body for the `SCANNER_ERROR` case so callers can
|
|
36
|
+
surface vendor-specific text. `ABORTED` is distinct from `TIMEOUT` so
|
|
37
|
+
UI copy that branches on "scanner timed out" doesn't mislabel a user-
|
|
38
|
+
cancelled probe.
|
|
39
|
+
- Optional `signal: AbortSignal` parameter on `scannerPresentDetailed` so
|
|
40
|
+
host apps can cancel an in-flight probe on retry / unmount instead of
|
|
41
|
+
waiting for the internal 5s timeout.
|
|
42
|
+
- `Api.test.js` — first test suite in this repo, using Node's built-in
|
|
43
|
+
`node:test` runner (zero new dependencies). Covers timeout, HTTP 4xx,
|
|
44
|
+
network error, scanner-error-in-body, invalid response, AbortSignal
|
|
45
|
+
propagation, happy path, and case-insensitive scheme detection.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
- Case-insensitive scheme detection in `postToScannerApi` URL builder
|
|
49
|
+
(VNT-1522). Previously `HTTPS://192.168.1.50` was treated as having no
|
|
50
|
+
scheme and double-prefixed to `http://HTTPS://...`; now matches `/^https?:\/\//i`
|
|
51
|
+
per RFC 3986.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
- `scannerPresent` (boolean) now delegates to `scannerPresentDetailed`.
|
|
55
|
+
One small behaviour difference: a failed Disconnect after a successful
|
|
56
|
+
Connect no longer flips the return to `false`. From the host app's
|
|
57
|
+
perspective the scanner IS present at that point, and the leaked
|
|
58
|
+
session times out server-side per the scanner's `IdleTimeout`. The
|
|
59
|
+
pre-refactor behaviour was a side-effect of the older try/catch wrap;
|
|
60
|
+
treating the disconnect as best-effort is the intended contract.
|
|
61
|
+
- `package.json` `test` script now runs Node's built-in `node --test`
|
|
62
|
+
runner instead of stubbing an error.
|
|
63
|
+
|
|
64
|
+
## [1.4.5-beta.10] - 2026-05-11
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
- iOS HPKE decryption failure on non-default hosts (e.g. ngrok tunnels,
|
|
68
|
+
custom staging domains). `startMobileVerification()` now derives the
|
|
69
|
+
holder page origin from `mobileVerifyBaseUrl` (falling back to the
|
|
70
|
+
CSR page's `window.location.origin` for same-origin dev setups) and
|
|
71
|
+
forwards it to `POST /sessions`. The backend uses this origin to
|
|
72
|
+
build its SessionTranscript, which must match the origin the mobile
|
|
73
|
+
wallet binds to. Without it, the server fell back to a per-environment
|
|
74
|
+
default that disagreed with the wallet's view, causing
|
|
75
|
+
`"Decryption failed: Could not decrypt credential response"` (400).
|
|
76
|
+
- `WebSocketClient.connect()` no longer hangs forever when the underlying
|
|
77
|
+
socket emits `onerror` or `onclose` before `onopen`. The returned Promise
|
|
78
|
+
now rejects with a tagged error in those paths so awaiters can handle
|
|
79
|
+
connection failures instead of waiting indefinitely.
|
|
80
|
+
- `startMobileVerification()` QR code URL construction now uses `new URL()`
|
|
81
|
+
to safely combine `mobileVerifyBaseUrl` with the session id. Previously,
|
|
82
|
+
a base URL containing a query string or hash (e.g. `https://host/path?x=1`)
|
|
83
|
+
produced an invalid result like `https://host/path?x=1/{session_id}`.
|
|
84
|
+
- `createVerificationSession()` no longer throws "body stream already
|
|
85
|
+
read" when an error response is not valid JSON. The response body is
|
|
86
|
+
now read once as text, then parsed in a try/catch instead of using
|
|
87
|
+
`response.json()` followed by a `response.text()` fallback.
|
|
88
|
+
- `startMobileVerification()` no longer emits duplicate `onError`
|
|
89
|
+
callbacks when a WebSocket failure surfaces via both
|
|
90
|
+
`WebSocketClient.connect()` and the outer catch. Errors already
|
|
91
|
+
reported by a lower layer carry an `error.reported` flag, and the
|
|
92
|
+
outer catch skips re-emitting in that case.
|
|
93
|
+
- `WebSocketClient.connect()` now invokes `callbacks.onError` from the
|
|
94
|
+
close-before-open path (previously only `onerror` did). Direct
|
|
95
|
+
`connectToVerificationSession` callers receive the failure event
|
|
96
|
+
even when no preceding `onerror` fires (e.g., server closes the
|
|
97
|
+
socket immediately). The synthesized error carries `reported: true`
|
|
98
|
+
so `startMobileVerification`'s outer catch still avoids double-emit.
|
|
99
|
+
- `WebSocketClient._handleMessage()` no longer logs the raw WebSocket
|
|
100
|
+
frame to the console. Verification payloads carry PII (mDL fields,
|
|
101
|
+
portrait base64), so the raw `console.log` of `data` was dropped.
|
|
102
|
+
Per-route type logs below remain for debug visibility.
|
|
103
|
+
- `WebSocketClient.connect()` now releases its socket reference when
|
|
104
|
+
`onerror` fires before `onopen`, so callers can immediately retry
|
|
105
|
+
`connect()` without hitting "WebSocket connection already exists"
|
|
106
|
+
in implementations where `onclose` doesn't fire after `onerror`.
|
|
107
|
+
- `WebSocketClient` now transitions to the `ERROR` state (not
|
|
108
|
+
`CLOSED`) when the socket closes before opening, so
|
|
109
|
+
`status`/`onStatusChange` reflect a connection failure rather than
|
|
110
|
+
a normal disconnect. Post-open closes still transition to `CLOSED`.
|
|
111
|
+
Now driven by an explicit `hasOpened` flag (set in `onopen`) — the
|
|
112
|
+
previous version used the Promise's `settled` flag, which is also
|
|
113
|
+
set by pre-open `onerror` and would incorrectly downgrade the
|
|
114
|
+
ERROR state to CLOSED when the follow-up onclose fired.
|
|
115
|
+
- `WebSocketClient.connect()` synchronous-throw rejections now carry
|
|
116
|
+
a stable `.code` (`NO_WEBSOCKET_IMPL` when the Node fallback fails,
|
|
117
|
+
`CONNECTION_FAILED` when the `WebSocket` constructor throws). The
|
|
118
|
+
thrown/rejected error now matches the structured `onError` payload,
|
|
119
|
+
so awaiters of the Promise can branch on the failure reason without
|
|
120
|
+
relying on the `onError` callback.
|
|
121
|
+
- `WebSocketClient.disconnect()` called while the socket is still
|
|
122
|
+
CONNECTING no longer reports a spurious connection error. The
|
|
123
|
+
`onclose` triggered by the user-initiated close used to flip
|
|
124
|
+
`status` from `closed` back to `error` and emit `onError`; it now
|
|
125
|
+
detects an instance-level `_closeRequested` flag set by
|
|
126
|
+
`disconnect()` and skips both. The pending `connect()` Promise
|
|
127
|
+
rejects with `code: 'CLIENT_DISCONNECTED'` so awaiters resolve.
|
|
128
|
+
- `WebSocketClient._handleMessage()` now decodes binary WebSocket
|
|
129
|
+
frames (Node `Buffer` from the `ws` package, browser `ArrayBuffer`
|
|
130
|
+
/ `Uint8Array` when `binaryType === 'arraybuffer'`) via `TextDecoder`
|
|
131
|
+
before `JSON.parse`. Previously, anything that wasn't a string
|
|
132
|
+
passed through as-is, so `routeKey` was always undefined and every
|
|
133
|
+
frame was silently dropped as "Unknown message route."
|
|
134
|
+
- Removed informational `console.log` calls from the mobile
|
|
135
|
+
verification code path (`WebSocketClient` + `startMobileVerification`).
|
|
136
|
+
Several of them embedded the `session_id` or QR target URL, which
|
|
137
|
+
is effectively a session access token and shouldn't be sent to
|
|
138
|
+
application logs / monitoring. Diagnostic `console.warn` and
|
|
139
|
+
`console.error` calls are preserved.
|
|
140
|
+
|
|
141
|
+
### Changed
|
|
142
|
+
- `startMobileVerification()` error callbacks now emit stage-specific
|
|
143
|
+
codes — `SESSION_CREATION_FAILED`, `QR_GENERATION_FAILED`, or
|
|
144
|
+
`WEBSOCKET_CONNECTION_FAILED` — so callers can distinguish which step
|
|
145
|
+
failed. Upstream `error.code` values are preserved when present.
|
|
146
|
+
|
|
147
|
+
### Internal
|
|
148
|
+
- The internal `createVerificationSession()` API helper gained an
|
|
149
|
+
optional third `origin` parameter; `startMobileVerification()` now
|
|
150
|
+
populates it from `mobileVerifyBaseUrl` (or `window.location.origin`
|
|
151
|
+
for same-origin dev). Host apps don't need to change anything — keep
|
|
152
|
+
calling `startMobileVerification()` as before.
|
|
153
|
+
- `package.json` now declares a `"browser": { "ws": false }` field so
|
|
154
|
+
browser bundlers (Vite / Webpack / Rollup) stub out the Node-only
|
|
155
|
+
`ws` optional dependency instead of trying to bundle it. Has no
|
|
156
|
+
effect in Node — the dynamic `import('ws')` fallback still works.
|
|
157
|
+
|
|
158
|
+
## [1.4.5-beta.9] - 2026-04-21
|
|
159
|
+
|
|
160
|
+
### Added
|
|
161
|
+
- Mobile ID verification support (VNT-1409, VNT-1419, VNT-1429, VNT-1455)
|
|
162
|
+
- Added `startMobileVerification()` function to initiate mobile verification sessions
|
|
163
|
+
- Added `createVerificationSession()` API function to create verification sessions with user ID and client ID
|
|
164
|
+
- Added WebSocket client (`WebSocketClient.js`) for real-time verification updates from AWS API Gateway
|
|
165
|
+
- Added mobile verification server address configuration with environment detection
|
|
166
|
+
- QR code generation for mobile verification sessions with customizable base URLs
|
|
167
|
+
- Real-time verification callbacks: `onVerificationStarted`, `onResult`, `onStatusChange`, `onError`
|
|
168
|
+
- Session management with session ID, session URL, WebSocket URL, and QR code SVG
|
|
169
|
+
- Connection state management (CONNECTING, OPEN, CLOSED, ERROR) for WebSocket connections
|
|
170
|
+
|
|
171
|
+
### Changed
|
|
172
|
+
- Updated environment detection to support mobile verification endpoints
|
|
173
|
+
- Mobile verification uses `staging.mdl.verantid.bluerocket.us` for staging
|
|
174
|
+
- Mobile verification uses `mdl.verantid.bluerocket.us` for production
|
|
175
|
+
- Updated scanner type detection to use `scannerProfile` from scanner response in auto-DMV verification
|
|
176
|
+
- Enhanced TypeScript definitions (`index.d.ts`) to include mobile verification types and callbacks
|
|
177
|
+
|
|
178
|
+
## [1.4.5-beta.8] - 2026-04-15
|
|
179
|
+
|
|
180
|
+
### Changed
|
|
181
|
+
- Moved UV/IR image type definitions to scanner profile configuration
|
|
182
|
+
- Added `frontUvImageType`, `backUvImageType`, `frontIrImageType`, and `backIrImageType` to IDXpressPlus profile
|
|
183
|
+
- Updated image extraction logic to use profile-defined UV/IR types instead of hardcoded strings
|
|
184
|
+
- Updated TypeScript definitions (`index.d.ts`) to include UV/IR base64 fields in `scanId` return type
|
|
185
|
+
- Removed UV/IR fields from local scanner functions (`localScanId`, `localContinueScanId`) as local scanners do not support UV/IR imaging
|
|
186
|
+
|
|
187
|
+
### Fixed
|
|
188
|
+
- Eliminated redundant variable declarations in `ScannerApi.js` by declaring UV/IR variables at function scope
|
|
189
|
+
|
|
190
|
+
## [1.4.5-beta.7] - 2026-04-15
|
|
191
|
+
|
|
192
|
+
### Changed
|
|
193
|
+
- Internal refactoring and code review fixes
|
|
194
|
+
|
|
195
|
+
## [1.4.5-beta.6] - 2026-04-15
|
|
196
|
+
|
|
197
|
+
### Added
|
|
198
|
+
- UV/IR image support for IDXpressPlus scanner profile
|
|
199
|
+
- Added `licenseFrontUvBase64`, `licenseBackUvBase64`, `licenseFrontIrBase64`, and `licenseBackIrBase64` fields to scan results
|
|
200
|
+
- Scanner now captures and returns ultraviolet (UV) and infrared (IR) images alongside standard color images
|
|
201
|
+
- Image types supported: fclr (front color), rclr (rear color), fclruv (front UV), rclruv (rear UV), fgsir (front IR), rgsir (rear IR)
|
|
202
|
+
|
|
203
|
+
### Fixed
|
|
204
|
+
- Image extraction logic now captures all image types from scanner response instead of only front/back color images
|
|
205
|
+
|
|
206
|
+
### Changed
|
|
207
|
+
- Updated IDXpressPlus scanner profile to include UV/IR parameters in default settings and restore configuration
|
|
208
|
+
- Added `clrUvIrResolution`, `fclrUvEnabled`, `rclrUvEnabled`, `fgsIrEnabled`, and `rgsIrEnabled` to `restoreParamKeys`
|
|
209
|
+
- Updated `ScannerApi.js` to extract UV/IR images from GetDocument response
|
|
210
|
+
- Updated `CloudScan.js` `scanId` function to return UV/IR image data (`licenseFrontUvBase64`, `licenseBackUvBase64`, `licenseFrontIrBase64`, `licenseBackIrBase64`)
|
|
211
|
+
- Local scanner functions (`localScanId`, `localContinueScanId`) do not include UV/IR fields as local scanners (VerantId6S) do not support UV/IR imaging
|
|
212
|
+
|
|
213
|
+
## [1.4.5-beta.5] - 2026-04-14
|
|
214
|
+
|
|
215
|
+
### Changed
|
|
216
|
+
- Applied dynamic environment URLs to face comparison feature
|
|
217
|
+
|
|
218
|
+
## [1.4.5-beta.4] - 2026-04-14
|
|
219
|
+
|
|
220
|
+
### Added
|
|
221
|
+
- Error validation for invalid environment type
|
|
222
|
+
|
|
223
|
+
### Changed
|
|
224
|
+
- Updated default server URLs to production endpoints
|
|
225
|
+
|
|
226
|
+
## [1.4.5-beta.3] - 2026-04-13
|
|
227
|
+
|
|
228
|
+
### Fixed
|
|
229
|
+
- Various bug fixes and code quality improvements
|
|
230
|
+
|
|
231
|
+
## [1.4.5-beta.2] - 2026-04-12
|
|
232
|
+
|
|
233
|
+
### Added
|
|
234
|
+
- Dynamic environment detection for server URLs
|
|
235
|
+
- Support for multiple deployment environments
|
|
236
|
+
|
|
237
|
+
### Changed
|
|
238
|
+
- Server address handling now supports trailing slashes
|
|
239
|
+
- Code improvements based on Copilot suggestions
|
|
240
|
+
|
|
241
|
+
## [1.4.5-beta.1] - 2026-04-11
|
|
242
|
+
|
|
243
|
+
### Added
|
|
244
|
+
- IDXpress scanner profiles support
|
|
245
|
+
- New scanner profile configuration options
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Version Number Guide
|
|
250
|
+
|
|
251
|
+
- **Major** (1.x.x): Breaking changes
|
|
252
|
+
- **Minor** (x.4.x): New features, backwards compatible
|
|
253
|
+
- **Patch** (x.x.5): Bug fixes, backwards compatible
|
|
254
|
+
- **Beta** (x.x.x-beta.x): Pre-release versions for testing
|