@verii/data-loader 1.0.0-pre.1752076816

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.
Files changed (55) hide show
  1. package/LICENSE +201 -0
  2. package/jest.config.js +20 -0
  3. package/package.json +45 -0
  4. package/src/batch-issuing/README.md +97 -0
  5. package/src/batch-issuing/constants.js +33 -0
  6. package/src/batch-issuing/fetchers.js +114 -0
  7. package/src/batch-issuing/file-readers.js +15 -0
  8. package/src/batch-issuing/file-writers.js +34 -0
  9. package/src/batch-issuing/index.js +124 -0
  10. package/src/batch-issuing/orchestrators.js +332 -0
  11. package/src/batch-issuing/prepare-data.js +167 -0
  12. package/src/batch-issuing/prompts.js +47 -0
  13. package/src/batch-issuing/validate-directory-exists.js +11 -0
  14. package/src/data-loader.js +14 -0
  15. package/src/helpers/common.js +30 -0
  16. package/src/helpers/compute-activation-date.js +12 -0
  17. package/src/helpers/index.js +8 -0
  18. package/src/helpers/load-csv.js +34 -0
  19. package/src/helpers/load-handlebars-template.js +9 -0
  20. package/src/helpers/parse-column.js +8 -0
  21. package/src/helpers/prepare-variable-sets.js +23 -0
  22. package/src/index.js +3 -0
  23. package/src/vendor-credentials/README.md +72 -0
  24. package/src/vendor-credentials/execute-update.js +32 -0
  25. package/src/vendor-credentials/index.js +49 -0
  26. package/src/vendor-credentials/orchestrator.js +22 -0
  27. package/src/vendor-credentials/prepare-data.js +36 -0
  28. package/test/batch-issuing.test.js +1523 -0
  29. package/test/data/badge-offer.template.json +22 -0
  30. package/test/data/batch-vars-offerids.csv +3 -0
  31. package/test/data/batch-vars.csv +3 -0
  32. package/test/data/batchissuing-variables.csv +3 -0
  33. package/test/data/driver-license-offer.template.json +10 -0
  34. package/test/data/driver-license-variables.csv +3 -0
  35. package/test/data/email-offer.template.json +10 -0
  36. package/test/data/email-variables.csv +3 -0
  37. package/test/data/id-document-offer.template.json +10 -0
  38. package/test/data/id-document-variables.csv +3 -0
  39. package/test/data/national-id-card-offer.template.json +10 -0
  40. package/test/data/national-id-card-variables.csv +3 -0
  41. package/test/data/offer.template.json +22 -0
  42. package/test/data/passport-offer.template.json +10 -0
  43. package/test/data/passport-variables.csv +3 -0
  44. package/test/data/person.template.json +15 -0
  45. package/test/data/phone-offer.template.json +10 -0
  46. package/test/data/phone-variables.csv +2 -0
  47. package/test/data/phones-batch-vars-offerids.csv +3 -0
  48. package/test/data/proof-of-age-offer.template.json +10 -0
  49. package/test/data/proof-of-age-variables.csv +3 -0
  50. package/test/data/resident-permit-offer.template.json +10 -0
  51. package/test/data/resident-permit-variables.csv +3 -0
  52. package/test/data/variables-no-did.csv +3 -0
  53. package/test/data/variables.csv +3 -0
  54. package/test/data/with-bom.csv +3 -0
  55. package/test/vendor-credentials.test.js +227 -0
@@ -0,0 +1,124 @@
1
+ const { program } = require('commander');
2
+ const { reduce } = require('lodash/fp');
3
+ const { printInfo, printError, parseColumn } = require('../helpers');
4
+ const { runBatchIssuing } = require('./orchestrators');
5
+
6
+ const parseVar = reduce((acc, pair) => {
7
+ const [key, value] = pair.split('=');
8
+ acc[key] = value;
9
+ return acc;
10
+ }, {});
11
+
12
+ program
13
+ .name('data-loader vendorcreds')
14
+ .description('Loads data into a db')
15
+ .usage('[options]')
16
+ .requiredOption(
17
+ '-c, --csv-filename <filename>',
18
+ 'file name containing variables'
19
+ )
20
+ .requiredOption(
21
+ '-o, --offer-template-filename <filename>',
22
+ 'file name containing the credential template file'
23
+ )
24
+ .requiredOption(
25
+ '-p, --path <path>',
26
+ 'the output directory to use where QR codes and output state files are stored'
27
+ )
28
+ .requiredOption(
29
+ '-t, --terms-url <termsUrl>',
30
+ 'the url to the T&Cs that holder must consent to'
31
+ )
32
+ .option(
33
+ '-d, --did <did>',
34
+ 'DID of the issuing organization. One of `tenant` or `did` must be specified.'
35
+ )
36
+ .option(
37
+ '-n, --tenant <tenantId>',
38
+ "Id of the issuing organization's tenant. One of `tenant` or `did` must be specified."
39
+ )
40
+ .option(
41
+ '-m, --identifier-match-column <identifierMatchColumn>',
42
+ `the column from the CSV for the user to be matched against the ID credential's "identifier" property
43
+ For example this should be the email column if matching against an Email credential type, or the phone number if
44
+ matching against a Phone credential type. Accepts header name or index. Default is 0.`,
45
+ parseColumn,
46
+ 0
47
+ )
48
+ .option(
49
+ '-u, --vendor-userid-column <vendorUseridColumn>',
50
+ `the column from the CSV that is users id. Value is made available as "vendorUserId" in the offer template. Accepts
51
+ header name or index. Default is 0.`,
52
+ parseColumn,
53
+ 0
54
+ )
55
+ .option(
56
+ '-e, --endpoint <url>',
57
+ 'Credential Agent Endpoint to call to execute the issuing'
58
+ )
59
+ .option(
60
+ '-a, --auth-token <url>',
61
+ 'Bearer Auth Token to be used on the Agent API'
62
+ )
63
+ .option('-l, --label <label>', 'A label to attach to the documents inserted')
64
+ .option(
65
+ '-v, --var <var...>',
66
+ 'A variable that will be injected into the credential template renderer. use name=value'
67
+ )
68
+ .option(
69
+ '-y, --credential-type <idCredentialType>',
70
+ 'the credential type used for identifying the user. Default is EmailV1.0.',
71
+ 'EmailV1.0'
72
+ )
73
+ .option(
74
+ '--purpose <purpose>',
75
+ 'The purpose to display to the user. Use a maximum for 64 chars. Default is "Career Credential Issuing"'
76
+ )
77
+ .option(
78
+ '--authTokenExpiresIn <authTokenExpiresIn>',
79
+ 'The number of minutes that the offer will be available for after activation. Default is 365 days.',
80
+ '525600'
81
+ )
82
+ .option('--new', 'Use a new disclosure for batch issuing')
83
+ .option(
84
+ '-i, --disclosure [disclosure]',
85
+ 'An existing disclosure to use for the batch issuing'
86
+ )
87
+ .option(
88
+ '--legacy',
89
+ 'the target credential agent is running in the "LEGACY" offer type mode. Default is false'
90
+ )
91
+ .option(
92
+ '-x --outputcsv',
93
+ "if passed an output csv is generated including the vendor's user id as the first column and the generated qrcode filename and deeplink"
94
+ )
95
+ .option(
96
+ '--x-name <outputCsvName>',
97
+ 'The file name for the output CSV. Default is "output"'
98
+ )
99
+ .option(
100
+ '--dryrun',
101
+ 'if passed in then a dry run executes showing how the data will be formatted'
102
+ )
103
+ .action(async () => {
104
+ const options = program.opts();
105
+ // eslint-disable-next-line better-mutation/no-mutation
106
+ options.vars = parseVar(options.var);
107
+ printInfo(options);
108
+ try {
109
+ await runBatchIssuing(options);
110
+ } catch (error) {
111
+ printError('Batch Issuing Script Failure');
112
+ if (error.response) {
113
+ printError({
114
+ error: error.message,
115
+ statusCode: error.response.statusCode,
116
+ errorCode: error.response.errorCode,
117
+ response: error.response.body,
118
+ });
119
+ } else {
120
+ printError(error);
121
+ }
122
+ }
123
+ })
124
+ .parseAsync(process.argv);
@@ -0,0 +1,332 @@
1
+ const { omitBy, isNil, find, isString, isEmpty, values } = require('lodash/fp');
2
+ const { validateDirectoryExists } = require('./validate-directory-exists');
3
+ const { initFetchers } = require('./fetchers');
4
+ const {
5
+ prepareNewDisclosureRequest,
6
+ prepareExchangeOffers,
7
+ } = require('./prepare-data');
8
+ const { printInfo } = require('../helpers/common');
9
+ const {
10
+ writeQrCodeFile,
11
+ writeJsonFile,
12
+ writeOutputCsv,
13
+ } = require('./file-writers');
14
+ const { CREDENTIAL_TYPES, DisclosureType } = require('./constants');
15
+ const {
16
+ askDisclosureType,
17
+ askDisclosureList,
18
+ askUseNewDisclosure,
19
+ } = require('./prompts');
20
+ const { loadCsv, getColName } = require('../helpers/load-csv');
21
+
22
+ const defaultOptions = {
23
+ vendorUseridColumn: 0,
24
+ identifierMatchColumn: 0,
25
+ authTokenExpiresIn: 525600,
26
+ legacy: false,
27
+ outputCsvName: 'output',
28
+ idCredentialType: 'EmailV1.0',
29
+ };
30
+
31
+ // eslint-disable-next-line consistent-return
32
+ const runBatchIssuing = async (opts) => {
33
+ const options = { ...defaultOptions, ...opts };
34
+ validateOptions(options);
35
+
36
+ const context = { fetchers: initFetchers(options) };
37
+ await setupDidOption(options, context);
38
+ const [csvHeaders, csvRows] = await loadCsv(options.csvFilename);
39
+
40
+ const disclosureRequest = await loadOrPrepareNewDisclosureRequest(
41
+ csvHeaders,
42
+ options,
43
+ context
44
+ );
45
+
46
+ const newExchangeOffers = await prepareExchangeOffers(
47
+ csvHeaders,
48
+ csvRows,
49
+ options
50
+ );
51
+
52
+ if (options.dryrun) {
53
+ printInfo('Dry Run would create:');
54
+ printInfo(
55
+ JSON.stringify(
56
+ omitBy(isNil, { disclosureRequest, newExchangeOffers }),
57
+ 0,
58
+ 2
59
+ )
60
+ );
61
+
62
+ return { disclosureRequest, newExchangeOffers };
63
+ }
64
+
65
+ const issuingDisclosure =
66
+ disclosureRequest.id != null
67
+ ? disclosureRequest
68
+ : await createDisclosureRequest(disclosureRequest, context);
69
+
70
+ await writeDisclosureToJson(issuingDisclosure, options);
71
+
72
+ const outputs = options.legacy
73
+ ? await runLegacyBatchIssuing(
74
+ issuingDisclosure,
75
+ newExchangeOffers,
76
+ options,
77
+ context
78
+ )
79
+ : await runSingleQrCodeBatchIssuing(
80
+ issuingDisclosure,
81
+ newExchangeOffers,
82
+ options,
83
+ context
84
+ );
85
+
86
+ writeOutput(outputs, {
87
+ ...options,
88
+ vendorUserIdColName: getColName(csvHeaders, options.vendorUseridColumn),
89
+ });
90
+ };
91
+
92
+ const loadExistingDisclosuresIfRequired = async (options, context) => {
93
+ if (options.new) {
94
+ return [];
95
+ }
96
+
97
+ const disclosures = await loadIntegratedIdentificationDisclosures(context);
98
+ if (options.disclosure) {
99
+ return disclosures;
100
+ }
101
+
102
+ // interactive mode is handled below
103
+
104
+ if (isEmpty(disclosures)) {
105
+ const useNewDisclosure = await askUseNewDisclosure();
106
+ if (!useNewDisclosure)
107
+ throw new Error(
108
+ 'no existing disclosures on the target agent. Use a new disclosure'
109
+ );
110
+
111
+ return [];
112
+ }
113
+
114
+ const disclosureType = await askDisclosureType();
115
+ return disclosureType === DisclosureType.NEW ? [] : disclosures;
116
+ };
117
+
118
+ const writeOutput = (outputs, options) => {
119
+ if (options.outputcsv && options.legacy) {
120
+ writeOutputCsv(outputs, options);
121
+ }
122
+ };
123
+
124
+ const runLegacyBatchIssuing = async (
125
+ disclosureRequest,
126
+ newExchangeOffers,
127
+ options,
128
+ context
129
+ ) => {
130
+ const outputs = [];
131
+ for (const newExchangeOffer of newExchangeOffers) {
132
+ outputs.push(
133
+ // eslint-disable-next-line no-await-in-loop
134
+ await createOfferExchangeAndQrCode(
135
+ {
136
+ ...newExchangeOffer,
137
+ disclosureRequest,
138
+ },
139
+ options,
140
+ context
141
+ )
142
+ );
143
+ }
144
+
145
+ return outputs;
146
+ };
147
+
148
+ const runSingleQrCodeBatchIssuing = async (
149
+ disclosureRequest,
150
+ newExchangeOffers,
151
+ options,
152
+ context
153
+ ) => {
154
+ const outputs = [];
155
+ for (const newExchangeOffer of newExchangeOffers) {
156
+ outputs.push(
157
+ // eslint-disable-next-line no-await-in-loop
158
+ await createOfferExchange(
159
+ {
160
+ ...newExchangeOffer,
161
+ disclosureRequest,
162
+ },
163
+ context
164
+ )
165
+ );
166
+ }
167
+
168
+ const { fetchers } = context;
169
+ printInfo('Generating qrcode & deep link');
170
+ const deeplink = await fetchers.loadDisclosureDeeplink(disclosureRequest);
171
+ printInfo(`Deep link: ${deeplink}`);
172
+ const qrcode = await fetchers.loadDisclosureQrcode(disclosureRequest);
173
+ const { filePath } = await writeQrCodeFile('qrcode-generic', qrcode, options);
174
+ printInfo(`QRCode saved: ${filePath}`);
175
+
176
+ return outputs;
177
+ };
178
+
179
+ const loadOrPrepareNewDisclosureRequest = async (
180
+ csvHeaders,
181
+ options,
182
+ context
183
+ ) => {
184
+ const disclosures = await loadExistingDisclosuresIfRequired(options, context);
185
+
186
+ if (isEmpty(disclosures)) {
187
+ return prepareNewDisclosureRequest(csvHeaders, options);
188
+ }
189
+
190
+ const disclosureId = isString(options.disclosure)
191
+ ? options.disclosure
192
+ : await askDisclosureList(disclosures);
193
+ const disclosure = find({ id: disclosureId }, disclosures);
194
+ if (disclosure == null) {
195
+ throw new Error('existing disclosure not found');
196
+ }
197
+ return disclosure;
198
+ };
199
+
200
+ const loadIntegratedIdentificationDisclosures = async ({ fetchers }) =>
201
+ fetchers.getDisclosureList(['integrated-issuing-identification']);
202
+
203
+ const createDisclosureRequest = async (newDisclosureRequest, { fetchers }) => {
204
+ return fetchers.createDisclosure(newDisclosureRequest);
205
+ };
206
+
207
+ const setupDidOption = async (options, { fetchers }) => {
208
+ if (options.did != null) {
209
+ return;
210
+ }
211
+ let did = 'did to be determined at runtime';
212
+ if (options.dryrun == null) {
213
+ const tenant = await fetchers.getTenant();
214
+ // eslint-disable-next-line better-mutation/no-mutation
215
+ ({ did } = tenant);
216
+ }
217
+ // eslint-disable-next-line better-mutation/no-mutation
218
+ options.did = did;
219
+ };
220
+
221
+ const writeDisclosureToJson = async (disclosureRequest, options) => {
222
+ printInfo(`Using disclosureId:${disclosureRequest.id}`);
223
+ printInfo('');
224
+
225
+ const { filePath: disclosureFilePath } = await writeJsonFile(
226
+ disclosureRequest,
227
+ `disclosure-${disclosureRequest.id}`,
228
+ options
229
+ );
230
+ await writeJsonFile(
231
+ {
232
+ disclosureId: disclosureRequest.id,
233
+ disclosureFile: disclosureFilePath,
234
+ timestamp: new Date().toISOString(),
235
+ ...options,
236
+ },
237
+ 'lastrun',
238
+ options
239
+ );
240
+ };
241
+
242
+ const createOfferExchangeAndQrCode = async (
243
+ { newExchange, newOffer, disclosureRequest },
244
+ options,
245
+ context
246
+ ) => {
247
+ const { fetchers } = context;
248
+ const { exchange, offer, vendorUserId } = await createOfferExchange(
249
+ {
250
+ newExchange,
251
+ newOffer,
252
+ disclosureRequest,
253
+ },
254
+ context
255
+ );
256
+
257
+ const deeplink = await fetchers.loadExchangeDeeplink(exchange);
258
+ const qrcode = await fetchers.loadExchangeQrcode(exchange);
259
+ const { filePath } = await writeQrCodeFile(
260
+ `qrcode-${vendorUserId}`,
261
+ qrcode,
262
+ options
263
+ );
264
+ printInfo(`${vendorUserId} Done. Qrcode file:${filePath}`);
265
+ printInfo('');
266
+
267
+ return {
268
+ exchange,
269
+ offer,
270
+ qrcode,
271
+ qrcodeFilePath: filePath,
272
+ deeplink,
273
+ vendorUserId,
274
+ };
275
+ };
276
+
277
+ const createOfferExchange = async (
278
+ { newExchange, newOffer, disclosureRequest },
279
+ context
280
+ ) => {
281
+ const { fetchers } = context;
282
+ const { vendorUserId } = newOffer.credentialSubject;
283
+ printInfo(`Setting up vendorUserId:${vendorUserId}`);
284
+ const exchange = await fetchers.createOfferExchange({
285
+ ...newExchange,
286
+ disclosureId: disclosureRequest.id,
287
+ });
288
+ const offer = await fetchers.createOffer(exchange, newOffer);
289
+ await fetchers.submitCompleteOffer(exchange, [offer]);
290
+ return {
291
+ exchange,
292
+ offer,
293
+ vendorUserId,
294
+ };
295
+ };
296
+
297
+ const validateOptions = (options) => {
298
+ if (options.dryrun == null && options.endpoint == null) {
299
+ throw new Error(
300
+ '"-e" or "--endpoint" is required unless executing a "dryrun"'
301
+ );
302
+ }
303
+ if (options.endpoint != null && options.authToken == null) {
304
+ throw new Error('"-a" or "--auth-token" is required');
305
+ }
306
+
307
+ validateTenantAndDidArgs(options);
308
+
309
+ validateDirectoryExists(options);
310
+
311
+ validateCredentialType(options.idCredentialType);
312
+ };
313
+
314
+ const validateTenantAndDidArgs = (options) => {
315
+ if (options.tenant == null && options.did == null) {
316
+ throw new Error('one of "--tenant" or "--did" is required');
317
+ }
318
+ };
319
+
320
+ const validateCredentialType = (idCredentialType) => {
321
+ const allowedIdCredentialTypes = values(CREDENTIAL_TYPES);
322
+ if (
323
+ !idCredentialType ||
324
+ !allowedIdCredentialTypes.includes(idCredentialType)
325
+ ) {
326
+ throw new Error(
327
+ `${idCredentialType} doesn't exist. Please use one of ${allowedIdCredentialTypes}`
328
+ );
329
+ }
330
+ };
331
+
332
+ module.exports = { runBatchIssuing };
@@ -0,0 +1,167 @@
1
+ const { get, includes, isNil, map, omitBy, isEmpty } = require('lodash/fp');
2
+ const { formatISO } = require('date-fns/fp');
3
+ const { nanoid } = require('nanoid');
4
+ const { loadHandlebarsTemplate, computeActivationDate } = require('../helpers');
5
+ const { idCredentialTypeToPick } = require('./constants');
6
+ const { getColIndex, prepareVariableSets } = require('../helpers');
7
+
8
+ const EMAIL_REGEX =
9
+ // eslint-disable-next-line max-len
10
+ /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
11
+ const PHONE_REGEX = /\(?([0-9]{3})\)?([ .-]?)([0-9]{3})\2([0-9]{4})/;
12
+
13
+ const VENDOR_USER_ID_PATH = 'credentialSubject.vendorUserId';
14
+
15
+ const prepareExchangeOffers = async (csvHeaders, csvRows, options) => {
16
+ const variableSets = await prepareVariableSets(csvHeaders, csvRows, options);
17
+ const offerTemplate = loadHandlebarsTemplate(options.offerTemplateFilename);
18
+ return map(
19
+ (variableSet) => ({
20
+ newOffer: prepareNewOffer({
21
+ template: offerTemplate,
22
+ variableSet,
23
+ ...options,
24
+ }),
25
+ newExchange: prepareNewExchange({ variableSet, ...options }),
26
+ }),
27
+ variableSets
28
+ );
29
+ };
30
+
31
+ const prepareNewOffer = ({ template, variableSet, label }) => {
32
+ validateVariableSet(variableSet);
33
+ const offerString = template(variableSet);
34
+ const offer = JSON.parse(offerString);
35
+ validateVendorUserIdInOffer(offer, variableSet);
36
+ return omitBy(isNil, {
37
+ ...offer,
38
+ offerId: variableSet.offerId ?? nanoid(),
39
+ label,
40
+ });
41
+ };
42
+
43
+ const validateVariableSet = (variableSet) => {
44
+ if (
45
+ variableSet.email == null &&
46
+ variableSet.phone == null &&
47
+ variableSet.identifier == null
48
+ ) {
49
+ throw new Error('"email", "phone", or "identifier" column must be defined');
50
+ }
51
+
52
+ const { email, phone, vendorUserId } = variableSet;
53
+
54
+ validateVendorUserIdVariable(vendorUserId);
55
+ validateEmailVariable(email);
56
+ validatePhoneVariable(phone);
57
+ };
58
+
59
+ const validateVendorUserIdVariable = (vendorUserId) => {
60
+ if (isEmpty(vendorUserId)) {
61
+ throw new Error(
62
+ 'vendorUserId variable must exist. Use the -u <column> option'
63
+ );
64
+ }
65
+ };
66
+
67
+ const validateEmailVariable = (email) => {
68
+ if (!email) return;
69
+
70
+ if (!EMAIL_REGEX.test(email)) {
71
+ throw new Error(`${email} is not a valid email`);
72
+ }
73
+ };
74
+
75
+ const validatePhoneVariable = (phone) => {
76
+ if (!phone) return;
77
+
78
+ if (!PHONE_REGEX.test(phone)) {
79
+ throw new Error(`${phone} is not a valid phone`);
80
+ }
81
+ };
82
+
83
+ const validateVendorUserIdInOffer = (offer, variableSet) => {
84
+ const value = get(VENDOR_USER_ID_PATH, offer);
85
+ if (!includes(value, variableSet)) {
86
+ throw new Error(
87
+ `${VENDOR_USER_ID_PATH}: ${value} cannot be hardcoded and must be defined in ${JSON.stringify(
88
+ variableSet
89
+ )})`
90
+ );
91
+ }
92
+ };
93
+
94
+ const matcherToIdCredentialType = (idCredentialType, variableSet) => {
95
+ const typeToVariable = {
96
+ 'EmailV1.0': variableSet.email,
97
+ 'PhoneV1.0': variableSet.phone,
98
+ 'DriversLicenseV1.0': variableSet.identifier,
99
+ 'IdDocumentV1.0': variableSet.identifier,
100
+ 'NationalIdCardV1.0': variableSet.identifier,
101
+ 'PassportV1.0': variableSet.identifier,
102
+ 'ProofOfAgeV1.0': variableSet.identifier,
103
+ 'ResidentPermitV1.0': variableSet.identifier,
104
+ };
105
+ return typeToVariable[idCredentialType] || variableSet.email;
106
+ };
107
+
108
+ const prepareNewExchange = ({ variableSet, label, idCredentialType }) =>
109
+ omitBy(isNil, {
110
+ type: 'ISSUING',
111
+ identityMatcherValues: [
112
+ matcherToIdCredentialType(idCredentialType, variableSet),
113
+ ],
114
+ label,
115
+ });
116
+
117
+ const prepareNewDisclosureRequest = (
118
+ csvHeaders,
119
+ {
120
+ label,
121
+ termsUrl,
122
+ purpose,
123
+ idCredentialType,
124
+ identifierMatchColumn,
125
+ vendorUseridColumn,
126
+ authTokenExpiresIn,
127
+ ...options
128
+ }
129
+ ) => {
130
+ const activationDate = computeActivationDate(options);
131
+
132
+ const newDisclosureRequest = {
133
+ offerMode: 'preloaded',
134
+ configurationType: 'issuing',
135
+ vendorEndpoint: 'integrated-issuing-identification',
136
+ types: [
137
+ {
138
+ type: idCredentialType,
139
+ },
140
+ ],
141
+ identityMatchers: {
142
+ rules: [
143
+ {
144
+ valueIndex: getColIndex(csvHeaders, identifierMatchColumn),
145
+ path: [idCredentialTypeToPick[idCredentialType]],
146
+ rule: 'pick',
147
+ },
148
+ ],
149
+ vendorUserIdIndex: getColIndex(csvHeaders, vendorUseridColumn),
150
+ },
151
+ setIssuingDefault: true,
152
+ duration: '1h', // 1 hour by default
153
+ vendorDisclosureId: Date.now(),
154
+ purpose: purpose ?? 'Issuing Career Credential', // by default have a generic message
155
+ activationDate: formatISO(activationDate),
156
+ authTokenExpiresIn: Number(authTokenExpiresIn),
157
+ termsUrl,
158
+ label,
159
+ };
160
+
161
+ return omitBy(isNil, newDisclosureRequest);
162
+ };
163
+
164
+ module.exports = {
165
+ prepareNewDisclosureRequest,
166
+ prepareExchangeOffers,
167
+ };
@@ -0,0 +1,47 @@
1
+ const { prompt } = require('inquirer');
2
+ const { format, parseISO } = require('date-fns');
3
+ const { take, map, flow } = require('lodash/fp');
4
+
5
+ const disclosureListQuestion = (disclosures) => ({
6
+ type: 'list',
7
+ name: 'disclosure',
8
+ message: 'Please select a disclosure',
9
+ choices: flow(
10
+ take(10),
11
+ map((disclosure) => ({
12
+ name: `${disclosure.purpose}, ${format(
13
+ parseISO(disclosure.createdAt),
14
+ 'MMM d yyyy h:mma'
15
+ )}`,
16
+ value: disclosure.id,
17
+ }))
18
+ )(disclosures),
19
+ });
20
+
21
+ const askQuestion = async (question) => {
22
+ const result = await prompt([question]);
23
+ return result[question.name];
24
+ };
25
+
26
+ const askDisclosureType = () =>
27
+ askQuestion({
28
+ type: 'list',
29
+ name: 'disclosureType',
30
+ message:
31
+ 'Would you like to use an existing disclosure or create a new one?',
32
+ choices: ['existing', 'new'],
33
+ });
34
+ const askUseNewDisclosure = () =>
35
+ askQuestion({
36
+ type: 'confirm',
37
+ name: 'useNewDisclosure',
38
+ message: 'The batch will create a new disclosure. Press enter to confirm.',
39
+ });
40
+ const askDisclosureList = (disclosures) =>
41
+ askQuestion(disclosureListQuestion(disclosures));
42
+
43
+ module.exports = {
44
+ askDisclosureType,
45
+ askDisclosureList,
46
+ askUseNewDisclosure,
47
+ };
@@ -0,0 +1,11 @@
1
+ const { existsSync } = require('fs');
2
+
3
+ const validateDirectoryExists = (options) => {
4
+ if (options.dryrun == null && !existsSync(options.path)) {
5
+ throw new Error('Path does not exist. Check the -p var');
6
+ }
7
+ };
8
+
9
+ module.exports = {
10
+ validateDirectoryExists,
11
+ };
@@ -0,0 +1,14 @@
1
+ const { program } = require('commander');
2
+ const packageJson = require('../package.json');
3
+
4
+ program
5
+ .version(packageJson.version, '-v, --vers', 'Current tool version')
6
+ .command('vendorcreds', 'load data from csv to the vendor', {
7
+ executableFile: `${__dirname}/vendor-credentials/index.js`,
8
+ })
9
+ .command('batchissuing', 'issue credentials from csv', {
10
+ executableFile: `${__dirname}/batch-issuing/index.js`,
11
+ })
12
+ .usage('[command] [options]')
13
+ .passThroughOptions()
14
+ .parse(process.argv);