verant_id_cloud_scan 1.4.4-beta.0 → 1.4.4-beta.2

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,136 +1,55 @@
1
+ import { compressAndCompareImages } from "./FaceComparison.js";
2
+ import { formatDateToISO } from "./DateUtils.js";
3
+ import { FIELD_MAPPING, FIELD_DEFAULTS, getFieldValue } from "./FieldMapping.js";
4
+
1
5
  //prod
2
6
  const licenseServerAddress = "https://lic.verantid.com/api/v1";
3
7
  const dmvServerAddress = "https://dmv.verantid.com/api/v1/dmv_check";
4
- //staging
8
+ // staging
5
9
  // const licenseServerAddress =
6
10
  // "https://verant-license-server-staging-52b03b060a98.herokuapp.com/api/v1";
7
11
  // const dmvServerAddress =
8
12
  // "https://dmv-check-server-staging-fcaab48bec21.herokuapp.com/api/v1/dmv_check";
9
13
 
10
- // Helper function that checks multiple possible keys and returns a non-empty value
11
- const getLicenseFieldValue = (data, possibleKeys) => {
12
- for (const key of possibleKeys) {
13
- if (data.hasOwnProperty(key)) {
14
- const value = data[key];
15
- if (value !== undefined && value !== null && value !== "") {
16
- return value;
17
- }
18
- }
19
- }
20
- return undefined;
21
- };
22
-
23
14
  export const dmvCheck = async (clientId, scannerType, licenseData) => {
24
15
  const url = dmvServerAddress; // make sure dmvServerAddress is defined elsewhere
25
16
 
26
17
  // Build the driver object dynamically.
27
18
  const driver = {};
28
19
 
29
- // Mapping of our driver field names to an array of possible keys
30
- // Optionally include a default value if no valid value is found.
31
- const fieldMapping = [
32
- {
33
- field: "jurisdiction_code",
34
- keys: [
35
- "jurisdiction_code",
36
- "jurisdictionCode",
37
- "JurisdictionCode",
38
- "MailingJurisdictionCode",
39
- ],
40
- },
41
- {
42
- field: "document_category_code",
43
- keys: [
44
- "document_category_code",
45
- "documentCategoryCode",
46
- "DocumentCategoryCode",
47
- ],
48
- default: "1", // default value if nothing is found
49
- },
50
- {
51
- field: "license_number",
52
- keys: ["license_number", "licenseNumber", "LicenseNumber", "LicenseOrIDNumber"],
53
- },
54
- {
55
- field: "expiration_date",
56
- keys: [
57
- "expiration_date",
58
- "expirationDate",
59
- "ExpirationDate",
60
- "LicenseExpirationDate",
61
- ],
62
- },
63
- {
64
- field: "issue_date",
65
- keys: [
66
- "issue_date",
67
- "issueDate",
68
- "IssueDate",
69
- "LicenseOrIDDocumentIssueDate",
70
- ],
71
- },
72
- {
73
- field: "first_name",
74
- keys: ["first_name", "firstName", "FirstName"],
75
- },
76
- {
77
- field: "last_name",
78
- keys: ["last_name", "lastName", "LastName", "FamilyName"],
79
- },
80
- {
81
- field: "birth_date",
82
- keys: ["birth_date", "birthDate", "BirthDate", "DateOfBirth"],
83
- },
84
- {
85
- field: "eye_color",
86
- keys: ["eye_color", "eyeColor", "EyeColor"],
87
- },
88
- {
89
- field: "sex_code",
90
- keys: ["sex_code", "sexCode", "SexCode", "sex", "Sex"],
91
- },
92
- {
93
- field: "address_line_1",
94
- keys: [
95
- "address_line_1",
96
- "addressLine1",
97
- "AddressLine1",
98
- "MailingStreetAddress1",
99
- ],
100
- },
101
- {
102
- field: "city",
103
- keys: ["city", "City", "MailingCity"],
104
- },
105
- {
106
- field: "state",
107
- keys: ["state", "State", "MailingJurisdictionCode"],
108
- },
109
- {
110
- field: "postal_code",
111
- keys: ["postal_code", "postalCode", "PostalCode", "MailingPostalCode"],
112
- },
113
- {
114
- field: "height",
115
- keys: ["height", "Height", "HeightInFT_IN", "HeightInCM"],
116
- },
117
- {
118
- field: "weight",
119
- keys: ["weight", "Weight", "WeightInLBS", "WeightInKG"],
120
- },
121
- ];
122
-
123
- // Iterate over each mapping. If a non-empty value is found (or a default exists), add the field.
124
- fieldMapping.forEach((mapping) => {
125
- let value = getLicenseFieldValue(licenseData, mapping.keys);
126
- if (value === undefined) {
127
- if (mapping.hasOwnProperty("default")) {
128
- value = mapping.default;
129
- } else {
130
- return; // Skip adding this field if no value (and no default) is found
20
+ // Iterate over each field in the mapping
21
+ Object.keys(FIELD_MAPPING).forEach((fieldName) => {
22
+ let value = getFieldValue(licenseData, fieldName);
23
+
24
+ // Use default value if no value found
25
+ if (value === undefined && FIELD_DEFAULTS.hasOwnProperty(fieldName)) {
26
+ value = FIELD_DEFAULTS[fieldName];
27
+ }
28
+
29
+ // Skip if still no value
30
+ if (value === undefined || value === null || value === '') {
31
+ return;
32
+ }
33
+
34
+ // Clean up the value (trim whitespace)
35
+ if (typeof value === 'string') {
36
+ value = value.trim();
37
+ // Skip if empty after trimming
38
+ if (value === '') {
39
+ return;
131
40
  }
132
41
  }
133
- driver[mapping.field] = value;
42
+
43
+ // Format date fields to ISO format (YYYY-MM-DD)
44
+ if (fieldName === 'birth_date' || fieldName === 'expiration_date' || fieldName === 'issue_date') {
45
+ value = formatDateToISO(value);
46
+ // Skip if date formatting returned empty string (invalid date)
47
+ if (value === '') {
48
+ return;
49
+ }
50
+ }
51
+
52
+ driver[fieldName] = value;
134
53
  });
135
54
 
136
55
  // Assemble the data payload.
@@ -153,13 +72,43 @@ export const dmvCheck = async (clientId, scannerType, licenseData) => {
153
72
  const responseData = await response.json();
154
73
  return responseData; // Return the response data
155
74
  } else {
156
- const errorText = await response.text(); // Read once and store
157
- console.error("DMV check failed:", errorText);
158
- return errorText; // Return the stored text
75
+ // Try to parse as JSON first (for structured error responses)
76
+ try {
77
+ const errorData = await response.json();
78
+ console.error("DMV check failed:", errorData);
79
+
80
+ // If there's a SOAP error message, try to extract a user-friendly message
81
+ let userMessage = errorData.message || 'DMV verification failed';
82
+
83
+ if (errorData.error_messages && typeof errorData.error_messages === 'string') {
84
+ // Try to extract text from SOAP fault
85
+ const reasonMatch = errorData.error_messages.match(/<s:Text[^>]*>([^<]+)<\/s:Text>/);
86
+ if (reasonMatch && reasonMatch[1]) {
87
+ userMessage = reasonMatch[1];
88
+ } else if (response.status === 503) {
89
+ userMessage = 'DMV verification service is temporarily unavailable. Please try again later.';
90
+ }
91
+ }
92
+
93
+ return { ...errorData, message: userMessage };
94
+ } catch (parseError) {
95
+ // If JSON parsing fails, fall back to text
96
+ const errorText = await response.text();
97
+ console.error("DMV check failed:", errorText);
98
+
99
+ let userMessage = 'DMV verification failed';
100
+ if (response.status === 503) {
101
+ userMessage = 'DMV verification service is temporarily unavailable. Please try again later.';
102
+ } else if (response.status === 500) {
103
+ userMessage = 'DMV verification service encountered an error. Please check your data and try again.';
104
+ }
105
+
106
+ return { status: 'error', message: userMessage, raw_error: errorText };
107
+ }
159
108
  }
160
109
  } catch (error) {
161
110
  console.error("There was a problem with the dmv_check request:", error);
162
- return null; // Error occurred
111
+ return { status: 'error', message: error.message || 'Unable to connect to DMV verification service.' };
163
112
  }
164
113
  };
165
114
 
@@ -256,6 +205,41 @@ export const checkLicense = async (clientId, macAddress) => {
256
205
  }
257
206
  };
258
207
 
208
+ /**
209
+ * Check if client has autoDmv feature enabled
210
+ *
211
+ * TODO: This will eventually call the /check_license endpoint and return
212
+ * the feature_licenses.autoDmv value. For now, we're manually returning true
213
+ * for testing purposes.
214
+ *
215
+ * @param {string} clientId - Client ID to check
216
+ * @returns {Promise<boolean>} True if autoDmv is enabled
217
+ */
218
+ export const checkAutoDmvEnabled = async (clientId) => {
219
+ const url = licenseServerAddress + "/check_feature_license";
220
+ const data = { client_id: clientId };
221
+
222
+ try {
223
+ const response = await fetch(url, {
224
+ method: "POST",
225
+ headers: {
226
+ "Content-Type": "application/json",
227
+ },
228
+ body: JSON.stringify(data),
229
+ });
230
+
231
+ const responseData = await response.json();
232
+ console.log("Response from checkAutoDmvEnabled:", responseData);
233
+
234
+ if (response.ok && responseData.dmv_verifications) {
235
+ return responseData.dmv_verifications.auto_dmv === true;
236
+ }
237
+ return false;
238
+ } catch (error) {
239
+ console.error("Error checking autoDmv feature:", error);
240
+ return false;
241
+ }
242
+ };
259
243
  export const getBarcodeLicenseKey = async () => {
260
244
  const url = licenseServerAddress + "/get_barcode_license_key";
261
245
 
@@ -379,4 +363,4 @@ export const checkScannerPresent = async (scannerAddress) => {
379
363
  // Any error means scanner is not reachable
380
364
  return false;
381
365
  }
382
- };
366
+ };
package/CloudScan.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  readBarcode,
5
5
  } from "./BarcodeScanner.js";
6
6
  import { driverLicenseFields } from "./DriverLicenseFields.js";
7
- import { localScannerChain, scanBack, scanFrontOnly } from "./LocalScannerApi.js";
7
+ import { localScannerChain, scanBack, scanFrontOnly, getCachedClientId } from "./LocalScannerApi.js";
8
8
  import { compressAndCompareImages, compressImage } from "./FaceComparison.js";
9
9
  import {
10
10
  checkDmvFeatureLicense,
@@ -14,6 +14,8 @@ import {
14
14
  scannerChain,
15
15
  scannerPresent,
16
16
  } from "./ScannerApi.js";
17
+ import { VerificationModal } from "./VerificationModal.js";
18
+ import { checkAutoDmvEnabled } from "./Api.js";
17
19
 
18
20
  let cachedLicenseFrontBase64 = "";
19
21
 
@@ -138,6 +140,33 @@ export const scanId = async (
138
140
  }
139
141
  barcodeResultsObject = extractInformation(res.barcodeResults);
140
142
  returnObj.barcodeResultsObject = barcodeResultsObject;
143
+
144
+ // Check if autoDmv is enabled and trigger modal
145
+ const autoDmvEnabled = await checkAutoDmvEnabled(clientId);
146
+ if (autoDmvEnabled) {
147
+ console.log('[AUTO-DMV] Feature enabled, showing verification modal');
148
+
149
+ // Determine scanner type - you can customize this based on your needs
150
+ const scannerType = 'DigitalCheck'; // Default scanner type
151
+
152
+ // Create and open the modal
153
+ const modal = new VerificationModal(clientId, scannerType, barcodeResultsObject, {
154
+ primaryColor: '#0074CB' // VerantID blue
155
+ });
156
+
157
+ try {
158
+ const dmvResult = await modal.open();
159
+ console.log('[AUTO-DMV] Modal result:', dmvResult);
160
+
161
+ // Attach DMV result to return object
162
+ returnObj.dmvVerificationResult = dmvResult;
163
+ } catch (error) {
164
+ console.error('[AUTO-DMV] Modal error:', error);
165
+ returnObj.dmvVerificationResult = { status: 'error', error: error.message };
166
+ }
167
+ } else {
168
+ console.log('[AUTO-DMV] Feature disabled for client:', clientId);
169
+ }
141
170
  }
142
171
  return returnObj;
143
172
  };
@@ -159,6 +188,26 @@ function getField(keyword, foundBarcodeString, beginNewline = true) {
159
188
  return subtext;
160
189
  }
161
190
 
191
+ /**
192
+ * Formats sex code for display
193
+ * Converts AAMVA numeric codes (1=Male, 2=Female) to letters (M, F)
194
+ * Preserves any other values unchanged
195
+ * @param {string} sexCode - The sex code from the barcode
196
+ * @returns {string} Formatted sex code
197
+ */
198
+ function formatSexCodeForDisplay(sexCode) {
199
+ if (!sexCode) return '';
200
+
201
+ const code = String(sexCode).trim();
202
+
203
+ // AAMVA standard: 1 = Male, 2 = Female
204
+ if (code === '1') return 'M';
205
+ if (code === '2') return 'F';
206
+
207
+ // Return original value for anything else (9, X, M, F, etc.)
208
+ return sexCode;
209
+ }
210
+
162
211
  //extracts the information from the barcode
163
212
  function extractInformation(foundBarcodeString) {
164
213
  var resultsArray = {};
@@ -167,7 +216,13 @@ function extractInformation(foundBarcodeString) {
167
216
  var item = driverLicenseFields[i];
168
217
  var fieldValue = getField(item.abbreviation, foundBarcodeString);
169
218
  if (fieldValue !== false) {
170
- resultsArray[item.description] = fieldValue;
219
+ // Format sex code for display while preserving original in OriginalSexCode
220
+ if (item.description === 'Sex') {
221
+ resultsArray['OriginalSexCode'] = fieldValue; // Store original for DMV verification
222
+ resultsArray[item.description] = formatSexCodeForDisplay(fieldValue);
223
+ } else {
224
+ resultsArray[item.description] = fieldValue;
225
+ }
171
226
  }
172
227
  if (item.abbreviation === "DAQ" && !fieldValue) {
173
228
  fieldValue = getField("DLDAQ", foundBarcodeString, false);
@@ -196,18 +251,15 @@ export const localScanId = async (scannerAddress, clientId) => {
196
251
  console.log("scanner address: " + scannerAddress);
197
252
  let scannerResponseObj = await localScannerChain(scannerAddress, clientId);
198
253
 
199
- //console.log(scannerResponseObj);
200
254
  if (scannerResponseObj.errorMessages.length > 0) {
201
255
  returnObj.errorMessages = scannerResponseObj.errorMessages;
202
256
  return returnObj;
203
- //return error
204
257
  }
205
258
  cachedLicenseFrontBase64 = scannerResponseObj.licenseFrontBase64;
206
- //console.log(cachedLicenseFrontBase64);
207
259
  return true;
208
260
  };
209
261
 
210
- export const localContinueScanId = async (scannerAddress, includeData, skipBackScan = false) => {
262
+ export const localContinueScanId = async (scannerAddress, includeData, clientId = null, skipBackScan = false) => {
211
263
  let barcodeResultsObject = {};
212
264
  let returnObj = {
213
265
  barcodeResultsObject,
@@ -229,7 +281,8 @@ export const localContinueScanId = async (scannerAddress, includeData, skipBackS
229
281
  else {
230
282
  scannerResponseObj = await scanBack(scannerAddress, includeData);
231
283
  }
232
-
284
+
285
+ //console.log(scannerResponseObj);
233
286
  if (scannerResponseObj.errorMessages.length > 0) {
234
287
  returnObj.errorMessages = scannerResponseObj.errorMessages;
235
288
  return returnObj;
@@ -285,6 +338,42 @@ export const localContinueScanId = async (scannerAddress, includeData, skipBackS
285
338
  }
286
339
  barcodeResultsObject = extractInformation(barcodeResponse.barcodeResults);
287
340
  returnObj.barcodeResultsObject = barcodeResultsObject;
341
+
342
+ // Check if autoDmv is enabled and trigger modal
343
+ // Use provided clientId, or fall back to cached clientId from localScanId
344
+ const effectiveClientId = clientId || getCachedClientId();
345
+
346
+ if (!effectiveClientId) {
347
+ console.warn('[AUTO-DMV] No clientId available. Auto-DMV verification will be skipped. Please ensure clientId is passed to localContinueScanId or that localScanId was called first.');
348
+ }
349
+
350
+ if (effectiveClientId) {
351
+ const autoDmvEnabled = await checkAutoDmvEnabled(effectiveClientId);
352
+ if (autoDmvEnabled) {
353
+ console.log('[AUTO-DMV] Feature enabled, showing verification modal');
354
+
355
+ // Determine scanner type - you can customize this based on your needs
356
+ const scannerType = 'VerantId6S'; // Default scanner type for local scanner
357
+
358
+ // Create and open the modal
359
+ const modal = new VerificationModal(effectiveClientId, scannerType, barcodeResultsObject, {
360
+ primaryColor: '#0074CB' // VerantID blue
361
+ });
362
+
363
+ try {
364
+ const dmvResult = await modal.open();
365
+ console.log('[AUTO-DMV] Modal result:', dmvResult);
366
+
367
+ // Attach DMV result to return object
368
+ returnObj.dmvVerificationResult = dmvResult;
369
+ } catch (error) {
370
+ console.error('[AUTO-DMV] Modal error:', error);
371
+ returnObj.dmvVerificationResult = { status: 'error', error: error.message };
372
+ }
373
+ } else {
374
+ console.log('[AUTO-DMV] Feature disabled for client:', effectiveClientId);
375
+ }
376
+ }
288
377
  }
289
378
  return returnObj;
290
379
  };
package/DateUtils.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Shared date formatting utilities
3
+ */
4
+
5
+ /**
6
+ * Formats a date from MMDDYYYY to YYYY-MM-DD (ISO format)
7
+ * Used for API submissions
8
+ * @param {string} mmddyyyy - Date string in MMDDYYYY format
9
+ * @returns {string} Date in YYYY-MM-DD format, or empty string if invalid
10
+ */
11
+ export const formatDateToISO = (mmddyyyy) => {
12
+ if (!mmddyyyy || mmddyyyy.length < 8) {
13
+ return "";
14
+ }
15
+ const month = mmddyyyy.slice(0, 2);
16
+ const day = mmddyyyy.slice(2, 4);
17
+ const year = mmddyyyy.slice(4, 8);
18
+ return `${year}-${month}-${day}`;
19
+ };
20
+
21
+ /**
22
+ * Formats a date from MMDDYYYY to MM/DD/YYYY (display format)
23
+ * Used for UI display
24
+ * @param {string} dateStr - Date string in MMDDYYYY, YYYY-MM-DD, or MM/DD/YYYY format
25
+ * @returns {string} Date in MM/DD/YYYY format, or original string if already formatted
26
+ */
27
+ export const formatDateToDisplay = (dateStr) => {
28
+ if (!dateStr || dateStr.length < 8) {
29
+ return dateStr;
30
+ }
31
+
32
+ // Already in MM/DD/YYYY or YYYY-MM-DD format
33
+ if (dateStr.includes('/') || dateStr.includes('-')) {
34
+ return dateStr;
35
+ }
36
+
37
+ // Convert MMDDYYYY to MM/DD/YYYY
38
+ const month = dateStr.slice(0, 2);
39
+ const day = dateStr.slice(2, 4);
40
+ const year = dateStr.slice(4, 8);
41
+
42
+ return `${month}/${day}/${year}`;
43
+ };
44
+
45
+ /**
46
+ * List of date field names
47
+ */
48
+ export const DATE_FIELDS = ['birth_date', 'expiration_date', 'issue_date'];
49
+
50
+ /**
51
+ * Checks if a field name represents a date field
52
+ * @param {string} fieldName - Field name to check
53
+ * @returns {boolean} True if field is a date field
54
+ */
55
+ export const isDateField = (fieldName) => {
56
+ return DATE_FIELDS.includes(fieldName);
57
+ };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Shared field mapping configuration for license data
3
+ * Maps standardized field names to various possible keys found in license data
4
+ */
5
+
6
+ export const FIELD_MAPPING = {
7
+ jurisdiction_code: ['jurisdiction_code', 'jurisdictionCode', 'JurisdictionCode', 'MailingJurisdictionCode'],
8
+ document_category_code: ['document_category_code', 'documentCategoryCode', 'DocumentCategoryCode'],
9
+ license_number: ['license_number', 'licenseNumber', 'LicenseNumber', 'LicenseOrIDNumber'],
10
+ expiration_date: ['expiration_date', 'expirationDate', 'ExpirationDate', 'LicenseExpirationDate'],
11
+ issue_date: ['issue_date', 'issueDate', 'IssueDate', 'LicenseOrIDDocumentIssueDate'],
12
+ first_name: ['first_name', 'firstName', 'FirstName'],
13
+ last_name: ['last_name', 'lastName', 'LastName', 'FamilyName'],
14
+ birth_date: ['birth_date', 'birthDate', 'BirthDate', 'DateOfBirth'],
15
+ eye_color: ['eye_color', 'eyeColor', 'EyeColor'],
16
+ sex_code: ['OriginalSexCode', 'sex_code', 'sexCode', 'SexCode', 'sex', 'Sex'],
17
+ address_line_1: ['address_line_1', 'addressLine1', 'AddressLine1', 'MailingStreetAddress1'],
18
+ city: ['city', 'City', 'MailingCity'],
19
+ state: ['state', 'State', 'MailingJurisdictionCode'],
20
+ postal_code: ['postal_code', 'postalCode', 'PostalCode', 'MailingPostalCode'],
21
+ height: ['height', 'Height', 'HeightInFT_IN', 'HeightInCM'],
22
+ weight: ['weight', 'Weight', 'WeightInLBS', 'WeightInKG']
23
+ };
24
+
25
+ /**
26
+ * Default values for certain fields
27
+ */
28
+ export const FIELD_DEFAULTS = {
29
+ document_category_code: '1'
30
+ };
31
+
32
+ /**
33
+ * Gets the value of a field from license data by checking multiple possible keys
34
+ * @param {Object} data - License data object
35
+ * @param {string} fieldName - Standardized field name (e.g., 'first_name')
36
+ * @returns {string|undefined} Field value or undefined if not found
37
+ */
38
+ export const getFieldValue = (data, fieldName) => {
39
+ const possibleKeys = FIELD_MAPPING[fieldName] || [fieldName];
40
+
41
+ for (const key of possibleKeys) {
42
+ if (data.hasOwnProperty(key)) {
43
+ const value = data[key];
44
+ if (value !== undefined && value !== null && value !== '') {
45
+ return value;
46
+ }
47
+ }
48
+ }
49
+ return undefined;
50
+ };
51
+
52
+ /**
53
+ * Field name labels for display
54
+ */
55
+ export const FIELD_LABELS = {
56
+ first_name: 'First Name',
57
+ last_name: 'Last Name',
58
+ license_number: 'License Number',
59
+ birth_date: 'Date of Birth',
60
+ address_line_1: 'Address',
61
+ expiration_date: 'Expiration Date',
62
+ issue_date: 'Issue Date',
63
+ eye_color: 'Eye Color',
64
+ sex_code: 'Sex',
65
+ height: 'Height',
66
+ weight: 'Weight',
67
+ city: 'City',
68
+ state: 'State',
69
+ postal_code: 'Postal Code',
70
+ jurisdiction_code: 'Jurisdiction',
71
+ document_category_code: 'Document Category'
72
+ };
@@ -8,6 +8,7 @@ let scanDataResults = {};
8
8
  let licenseFrontBase64 = "";
9
9
  let licenseBackBase64 = "";
10
10
  let imageCount = 0;
11
+ let cachedClientId = "";
11
12
 
12
13
  function buildError(errorText) {
13
14
  if (errorText.includes("Invalid URL")) {
@@ -26,6 +27,7 @@ function buildError(errorText) {
26
27
  export async function localScannerChain(ambirApiAddress, clientId) {
27
28
  errorMessages = [];
28
29
  localApiAddress = ambirApiAddress;
30
+ cachedClientId = clientId; // Cache clientId for use in localContinueScanId
29
31
 
30
32
  //0 get list of local scanners
31
33
  const localScannerResults = await getLocalScanners();
@@ -507,3 +509,8 @@ async function clearImageAndCloseScannerConnection() {
507
509
  return false;
508
510
  }
509
511
  }
512
+
513
+ // Getter for cached clientId
514
+ export function getCachedClientId() {
515
+ return cachedClientId;
516
+ }