verant_id_cloud_scan 1.4.5-beta.11 → 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 CHANGED
@@ -1,4 +1,4 @@
1
- import { compressAndCompareImages } from "./FaceComparison.js";
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
- * Get license server address based on current environment
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
- const env = getEnvironment();
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
- * Get DMV server address based on current environment
77
- * @returns {string}
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
- 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";
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
- 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";
108
+ return `${host("face")}/compare_id_and_face`;
95
109
  }
96
110
 
97
111
  /**
98
- * Get Mobile Verification server address based on current environment
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
- const env = getEnvironment();
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) => {
package/Api.test.js CHANGED
@@ -14,7 +14,9 @@ import {
14
14
  checkScannerPresentDetailed,
15
15
  buildSecurelinkUrl,
16
16
  getNetworkInfo,
17
+ dmvCheck,
17
18
  } from "./Api.js";
19
+ import { FIELD_MAPPING, FIELD_LABELS, getFieldValue } from "./FieldMapping.js";
18
20
 
19
21
  let originalFetch;
20
22
 
@@ -336,3 +338,66 @@ describe("postToScannerApi (throw-on-error wrapper) — JSON contract (VNT-1522)
336
338
  );
337
339
  });
338
340
  });
341
+
342
+ describe("dmvCheck — personDigitalImage passthrough (VNT-1464)", () => {
343
+ // A base64 data URI that includes '+', '/', and '=' padding, to prove the
344
+ // value is forwarded byte-for-byte (no sanitization / no re-encoding).
345
+ const PORTRAIT =
346
+ "data:image/jpeg;base64,/9j/4AAQSkZJRg+ABAQEAYABgAAD/2wBDAA==";
347
+
348
+ it("forwards personDigitalImage into the driver object unchanged", async () => {
349
+ let sentBody = null;
350
+ globalThis.fetch = async (_url, init) => {
351
+ sentBody = JSON.parse(init.body);
352
+ return jsonResponse({ status: "success", match_status: true });
353
+ };
354
+
355
+ await dmvCheck("client-1", "Upload", {
356
+ jurisdiction_code: "P6",
357
+ license_number: "D123",
358
+ personDigitalImage: PORTRAIT,
359
+ });
360
+
361
+ assert.equal(
362
+ sentBody.driver.person_digital_image,
363
+ PORTRAIT,
364
+ "base64 portrait should pass through byte-for-byte",
365
+ );
366
+ });
367
+
368
+ it("does not send person_digital_image when it is absent", async () => {
369
+ let sentBody = null;
370
+ globalThis.fetch = async (_url, init) => {
371
+ sentBody = JSON.parse(init.body);
372
+ return jsonResponse({ status: "success" });
373
+ };
374
+
375
+ await dmvCheck("client-1", "Upload", {
376
+ jurisdiction_code: "P6",
377
+ license_number: "D123",
378
+ });
379
+
380
+ assert.equal(
381
+ Object.prototype.hasOwnProperty.call(sentBody.driver, "person_digital_image"),
382
+ false,
383
+ "absent field should not be added to the driver",
384
+ );
385
+ });
386
+ });
387
+
388
+ describe("FieldMapping — person_digital_image (VNT-1464)", () => {
389
+ it("resolves the value from every supported key variant", () => {
390
+ for (const key of [
391
+ "person_digital_image",
392
+ "personDigitalImage",
393
+ "PersonDigitalImage",
394
+ ]) {
395
+ assert.equal(getFieldValue({ [key]: "IMG" }, "person_digital_image"), "IMG");
396
+ }
397
+ });
398
+
399
+ it("is registered in FIELD_MAPPING and labeled 'License Portrait'", () => {
400
+ assert.ok(FIELD_MAPPING.person_digital_image, "should exist in FIELD_MAPPING");
401
+ assert.equal(FIELD_LABELS.person_digital_image, "License Portrait");
402
+ });
403
+ });
package/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.4.5-beta.13] - 2026-06-26
9
+
10
+ ### Added
11
+ - **`extractPortraitFromLicense(clientId, licenseImage)`** (VNT-1464 / VNT-1470).
12
+ Extracts the cropped portrait from a license image via the face lambda's
13
+ dedicated `/extract_portrait` endpoint — sends only the license image, no
14
+ comparison. Resolves to `{ extractedPortrait, error? }` where
15
+ `extractedPortrait` is a base64 JPEG data URI or `null` when no face is
16
+ detected. Exported from `CloudScan.js` and typed in `index.d.ts`.
17
+ - **`getExtractPortraitServerAddress()`** — resolves the `/extract_portrait`
18
+ endpoint for the current environment.
19
+ - **`person_digital_image` field support** (VNT-1464). Added to `FIELD_MAPPING`
20
+ (key variants `person_digital_image` / `personDigitalImage` /
21
+ `PersonDigitalImage`) and `FIELD_LABELS` (`"License Portrait"`). `dmvCheck`
22
+ forwards it in the driver payload when present; `VerificationModal` renders
23
+ `"Photo"` for the field on the confirmation step instead of the base64 string.
24
+
25
+ ### Changed
26
+ - Renamed `FaceComparison.js` → `FaceApi.js`; it now houses both the face
27
+ comparison and portrait-extraction clients. No public API change — the
28
+ library entry point still exports `startFaceComparison` and
29
+ `extractPortraitFromLicense`.
30
+ - Centralized per-environment host resolution in `Api.js` behind a single
31
+ `HOSTS` map and a `host(service)` helper. The address getters now append
32
+ their path to a per-service host instead of each branching on environment.
33
+
8
34
  ## [1.4.5-beta.11] - 2026-05-21
9
35
 
10
36
  ### Added
package/CloudScan.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  } from "./BarcodeScanner.js";
6
6
  import { driverLicenseFields } from "./DriverLicenseFields.js";
7
7
  import { localScannerChain, scanBack, scanFrontOnly, getCachedClientId } from "./LocalScannerApi.js";
8
- import { compressAndCompareImages, compressImage } from "./FaceComparison.js";
8
+ import { compressAndCompareImages, compressImage } from "./FaceApi.js";
9
9
  import {
10
10
  checkDmvFeatureLicense,
11
11
  checkFaceComparisonFeatureLicense,
@@ -39,6 +39,8 @@ export const startFaceComparison = async (clientId, licenseSrc, faceSrc) => {
39
39
  return await compressAndCompareImages(clientId, licenseSrc, faceSrc);
40
40
  };
41
41
 
42
+ export { extractPortraitFromLicense } from "./FaceApi.js";
43
+
42
44
  export const checkFaceCompareFeature = async (clientId) => {
43
45
  return await checkFaceComparisonFeatureLicense(clientId);
44
46
  }
@@ -1,4 +1,4 @@
1
- import { getFaceComparisonServerAddress } from "./Api.js";
1
+ import { getFaceComparisonServerAddress, getExtractPortraitServerAddress } from "./Api.js";
2
2
 
3
3
 
4
4
  export async function compressImage(source, maxSize = 2 * 1024 * 1024) {
@@ -98,6 +98,45 @@ export async function compressAndCompareImages(
98
98
  return result;
99
99
  }
100
100
 
101
+ /**
102
+ * Extracts a portrait from a license image
103
+ *
104
+ * @param {string} clientId
105
+ * @param {string|File|HTMLCanvasElement} licenseInput
106
+ * @param {number} [maxSize] max compressed size in bytes (defaults to 2 MB)
107
+ * @returns {Promise<{ extractedPortrait: string|null, error?: { error: any, details: any } }>}
108
+ */
109
+ export async function extractPortraitFromLicense(
110
+ clientId,
111
+ licenseInput,
112
+ maxSize = 2 * 1024 * 1024
113
+ ) {
114
+ let licSource = licenseInput;
115
+ if (typeof licSource === 'string') {
116
+ licSource = await base64ToCanvas(licSource);
117
+ }
118
+ const licenseBase64 = await compressImage(licSource, maxSize);
119
+
120
+ const res = await fetch(getExtractPortraitServerAddress(), {
121
+ method: "POST",
122
+ headers: { "Content-Type": "application/json" },
123
+ body: JSON.stringify({
124
+ client_id: clientId,
125
+ license_front: `data:image/jpeg;base64,${licenseBase64}`,
126
+ }),
127
+ });
128
+
129
+ const data = await res.json();
130
+
131
+ const result = { extractedPortrait: data?.extractedPortrait ?? null };
132
+
133
+ if (data?.error || data?.details) {
134
+ result.error = { error: data?.error, details: data?.details };
135
+ }
136
+
137
+ return result;
138
+ }
139
+
101
140
  async function base64ToCanvas(src) {
102
141
  // if it’s missing the data: prefix, add it
103
142
  const dataUrl = src.startsWith('data:') ? src : `data:image/jpeg;base64,${src}`;
package/FieldMapping.js CHANGED
@@ -19,7 +19,8 @@ export const FIELD_MAPPING = {
19
19
  state: ['state', 'State', 'MailingJurisdictionCode'],
20
20
  postal_code: ['postal_code', 'postalCode', 'PostalCode', 'MailingPostalCode'],
21
21
  height: ['OriginalHeight', 'height', 'Height', 'HeightInFT_IN', 'HeightInCM'],
22
- weight: ['weight', 'Weight', 'WeightInLBS', 'WeightInKG']
22
+ weight: ['weight', 'Weight', 'WeightInLBS', 'WeightInKG'],
23
+ person_digital_image: ['person_digital_image', 'personDigitalImage', 'PersonDigitalImage']
23
24
  };
24
25
 
25
26
  /**
@@ -68,5 +69,6 @@ export const FIELD_LABELS = {
68
69
  state: 'State',
69
70
  postal_code: 'Postal Code',
70
71
  jurisdiction_code: 'Jurisdiction',
71
- document_category_code: 'Document Category'
72
+ document_category_code: 'Document Category',
73
+ person_digital_image: 'License Portrait'
72
74
  };
@@ -503,6 +503,10 @@ export class VerificationModal {
503
503
  return 'N/A';
504
504
  }
505
505
 
506
+ if (fieldName === 'person_digital_image') {
507
+ return 'Photo';
508
+ }
509
+
506
510
  let displayValue = String(value).trim();
507
511
 
508
512
  // Format date fields to MM/DD/YYYY
package/index.d.ts CHANGED
@@ -19,6 +19,14 @@ declare module "verant_id_cloud_scan" {
19
19
  range: string;
20
20
  }>;
21
21
 
22
+ export function extractPortraitFromLicense(
23
+ clientId: string,
24
+ licenseImage: string | File | HTMLCanvasElement,
25
+ ): Promise<{
26
+ extractedPortrait: string | null;
27
+ error?: { error: any; details: any };
28
+ }>;
29
+
22
30
  export function readImageBarcode(
23
31
  imageFile: File,
24
32
  clientId?: string | null,
@@ -80,13 +88,13 @@ declare module "verant_id_cloud_scan" {
80
88
  }>;
81
89
 
82
90
  // Mobile Verification Types
83
- export type ConnectionStateValue = 'connecting' | 'open' | 'closed' | 'error';
91
+ export type ConnectionStateValue = "connecting" | "open" | "closed" | "error";
84
92
 
85
93
  export const ConnectionState: {
86
- readonly CONNECTING: 'connecting';
87
- readonly OPEN: 'open';
88
- readonly CLOSED: 'closed';
89
- readonly ERROR: 'error';
94
+ readonly CONNECTING: "connecting";
95
+ readonly OPEN: "open";
96
+ readonly CLOSED: "closed";
97
+ readonly ERROR: "error";
90
98
  };
91
99
 
92
100
  export interface VerificationCallbacks {
@@ -127,14 +135,14 @@ declare module "verant_id_cloud_scan" {
127
135
  * use `startMobileVerification`.
128
136
  */
129
137
  export function connectToVerificationSession(
130
- options: ConnectToVerificationSessionOptions
138
+ options: ConnectToVerificationSessionOptions,
131
139
  ): Promise<ConnectedSession>;
132
140
 
133
141
  export function startMobileVerification(
134
142
  userId: string,
135
143
  clientId: string,
136
144
  callbacks?: VerificationCallbacks,
137
- mobileVerifyBaseUrl?: string
145
+ mobileVerifyBaseUrl?: string,
138
146
  ): Promise<VerificationSession>;
139
147
 
140
148
  export class WebSocketClient {
@@ -234,4 +242,4 @@ export function scannerPresentDetailed(
234
242
  * Only verify-staging.verantid.com automatically uses staging.
235
243
  * @param environment - 'staging' or 'production'
236
244
  */
237
- export function setEnvironment(environment: 'staging' | 'production'): void;
245
+ export function setEnvironment(environment: "staging" | "production"): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verant_id_cloud_scan",
3
- "version": "1.4.5-beta.11",
3
+ "version": "1.4.5-beta.13",
4
4
  "description": "Verant ID Cloud Scan NPM Library",
5
5
  "main": "CloudScan.js",
6
6
  "types": "index.d.ts",