@velocitycareerlabs/data-loader 1.19.0-dev-build.11b7cc7b6
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/LICENSE +248 -0
- package/jest.config.js +7 -0
- package/package.json +43 -0
- package/src/batch-issuing/README.md +95 -0
- package/src/batch-issuing/constants.js +8 -0
- package/src/batch-issuing/fetchers.js +100 -0
- package/src/batch-issuing/file-readers.js +15 -0
- package/src/batch-issuing/file-writers.js +37 -0
- package/src/batch-issuing/index.js +115 -0
- package/src/batch-issuing/orchestrators.js +258 -0
- package/src/batch-issuing/prepare-data.js +208 -0
- package/src/batch-issuing/prompts.js +47 -0
- package/src/batch-issuing/validate-directory-exists.js +11 -0
- package/src/data-loader.js +14 -0
- package/src/helpers/common.js +30 -0
- package/src/helpers/compute-activation-date.js +12 -0
- package/src/helpers/index.js +6 -0
- package/src/helpers/load-csv.js +29 -0
- package/src/helpers/load-handlebars-template.js +9 -0
- package/src/index.js +3 -0
- package/src/vendor-credentials/README.md +32 -0
- package/src/vendor-credentials/execute-update.js +32 -0
- package/src/vendor-credentials/index.js +53 -0
- package/src/vendor-credentials/prepare-data.js +40 -0
- package/test/batch-issuing.test.js +916 -0
- package/test/data/badge-offer.template.json +22 -0
- package/test/data/batch-vars-offerids.csv +3 -0
- package/test/data/batch-vars.csv +3 -0
- package/test/data/batchissuing-variables.csv +3 -0
- package/test/data/driver-license-offer.template.json +10 -0
- package/test/data/driver-license-variables.csv +3 -0
- package/test/data/email-offer.template.json +10 -0
- package/test/data/email-variables.csv +3 -0
- package/test/data/id-document-offer.template.json +10 -0
- package/test/data/id-document-variables.csv +3 -0
- package/test/data/national-id-card-offer.template.json +10 -0
- package/test/data/national-id-card-variables.csv +3 -0
- package/test/data/offer.template.json +22 -0
- package/test/data/passport-offer.template.json +10 -0
- package/test/data/passport-variables.csv +3 -0
- package/test/data/person.template.json +15 -0
- package/test/data/phone-offer.template.json +10 -0
- package/test/data/phone-variables.csv +2 -0
- package/test/data/phones-batch-vars-offerids.csv +3 -0
- package/test/data/proof-of-age-offer.template.json +10 -0
- package/test/data/proof-of-age-variables.csv +3 -0
- package/test/data/resident-permit-offer.template.json +10 -0
- package/test/data/resident-permit-variables.csv +3 -0
- package/test/data/variables-no-did.csv +3 -0
- package/test/data/variables.csv +3 -0
- package/test/vendor-credentials.test.js +225 -0
@@ -0,0 +1,115 @@
|
|
1
|
+
const { program } = require('commander');
|
2
|
+
const { reduce } = require('lodash/fp');
|
3
|
+
const { printInfo, printError } = require('../helpers/common');
|
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
|
+
const validateOptions = (options) => {
|
13
|
+
if (options.dryrun == null && options.endpoint == null) {
|
14
|
+
throw new Error(
|
15
|
+
'"-e" or "--endpoint" is required unless executing a "dryrun"'
|
16
|
+
);
|
17
|
+
}
|
18
|
+
if (options.endpoint != null && options.authToken == null) {
|
19
|
+
throw new Error('"-a" or "--auth-token" is required');
|
20
|
+
}
|
21
|
+
};
|
22
|
+
|
23
|
+
program
|
24
|
+
.name('data-loader vendorcreds')
|
25
|
+
.description('Loads data into a db')
|
26
|
+
.usage('[options]')
|
27
|
+
.requiredOption(
|
28
|
+
'-c, --csv-filename <filename>',
|
29
|
+
'File name containing variables'
|
30
|
+
)
|
31
|
+
.requiredOption(
|
32
|
+
'-o, --offer-template-filename <filename>',
|
33
|
+
'File name containing the credential template file'
|
34
|
+
)
|
35
|
+
.requiredOption(
|
36
|
+
'-d, --did <did>',
|
37
|
+
'DID of the organization that should do the issuing'
|
38
|
+
)
|
39
|
+
.requiredOption(
|
40
|
+
'-p, --path <path>',
|
41
|
+
'path of directory for the qr-code images and output CSV'
|
42
|
+
)
|
43
|
+
.requiredOption(
|
44
|
+
'-t, --terms-url <termsUrl>',
|
45
|
+
'The terms and conditions url for the holder to accept'
|
46
|
+
)
|
47
|
+
.option(
|
48
|
+
'--legacy',
|
49
|
+
'the target credential agent is running in the "LEGACY" offer type mode. Default is false'
|
50
|
+
)
|
51
|
+
.option(
|
52
|
+
'-x --outputcsv',
|
53
|
+
"if passed an output csv is generated including the vendor's user id as the first column and the generated qrcode filename and deeplink"
|
54
|
+
)
|
55
|
+
.option(
|
56
|
+
'--x-name <outputCsvName>',
|
57
|
+
'The file name for the output CSV. Default is "output"'
|
58
|
+
)
|
59
|
+
.option(
|
60
|
+
'--x-vendor-userid-name <vendorUserIdName>',
|
61
|
+
'The column name to use for the vendor user id in the output csv. Default is "email"'
|
62
|
+
)
|
63
|
+
.option(
|
64
|
+
'--dryrun',
|
65
|
+
'if passed in then a dry run executes showing how the data will be formatted'
|
66
|
+
)
|
67
|
+
.option(
|
68
|
+
'-e, --endpoint <url>',
|
69
|
+
'Credential Agent Endpoint to call to execute the issuing'
|
70
|
+
)
|
71
|
+
.option('-a, --auth-token <url>', 'Bearer Auth Token to use')
|
72
|
+
.option('-l, --label <label>', 'A label to attach to the documents inserted')
|
73
|
+
.option('-v, --var <var...>', 'A variable to add. use name=value')
|
74
|
+
.option(
|
75
|
+
'-y, --credential-type <credentialType>',
|
76
|
+
'The type of credentials. Default is EmailV1.0.',
|
77
|
+
'EmailV1.0'
|
78
|
+
)
|
79
|
+
.option(
|
80
|
+
'--purpose <purpose>',
|
81
|
+
'The purpose to display to the user. Use a maximum for 64 chars. Default is "Career Credential Issuing"'
|
82
|
+
)
|
83
|
+
.option(
|
84
|
+
'--authTokenExpiresIn <authTokenExpiresIn>',
|
85
|
+
'The number of minutes that the offer will be available for after activation. Default is 90 days.',
|
86
|
+
'10080'
|
87
|
+
)
|
88
|
+
.option('--new', 'Use a new disclosure for batch issuing')
|
89
|
+
.option(
|
90
|
+
'-i, --disclosure [disclosure]',
|
91
|
+
'An existing disclosure to use for the batch issuing'
|
92
|
+
)
|
93
|
+
.action(async () => {
|
94
|
+
const options = program.opts();
|
95
|
+
validateOptions(options);
|
96
|
+
// eslint-disable-next-line better-mutation/no-mutation
|
97
|
+
options.vars = parseVar(options.var);
|
98
|
+
printInfo(options);
|
99
|
+
try {
|
100
|
+
await runBatchIssuing(options);
|
101
|
+
} catch (error) {
|
102
|
+
printError('Batch Issuing Script Failure');
|
103
|
+
if (error.response) {
|
104
|
+
printError({
|
105
|
+
error: error.message,
|
106
|
+
statusCode: error.response.statusCode,
|
107
|
+
errorCode: error.response.errorCode,
|
108
|
+
response: error.response.body,
|
109
|
+
});
|
110
|
+
} else {
|
111
|
+
printError(error);
|
112
|
+
}
|
113
|
+
}
|
114
|
+
})
|
115
|
+
.parseAsync(process.argv);
|
@@ -0,0 +1,258 @@
|
|
1
|
+
const { omitBy, isNil, find, isString, isEmpty } = require('lodash/fp');
|
2
|
+
const { validateDirectoryExists } = require('./validate-directory-exists');
|
3
|
+
const { initFetchers } = require('./fetchers');
|
4
|
+
const { prepareData } = require('./prepare-data');
|
5
|
+
const { printInfo } = require('../helpers/common');
|
6
|
+
const {
|
7
|
+
writeQrCodeFile,
|
8
|
+
writeJsonFile,
|
9
|
+
writeOutputCsv,
|
10
|
+
} = require('./file-writers');
|
11
|
+
const { DisclosureType } = require('./constants');
|
12
|
+
const {
|
13
|
+
askDisclosureType,
|
14
|
+
askDisclosureList,
|
15
|
+
askUseNewDisclosure,
|
16
|
+
} = require('./prompts');
|
17
|
+
|
18
|
+
// eslint-disable-next-line complexity
|
19
|
+
const getDisclosureType = async (hasIntegratedDisclosures, options) => {
|
20
|
+
const isIntegratedMode = !options.new && !options.disclosure;
|
21
|
+
|
22
|
+
switch (true) {
|
23
|
+
case isIntegratedMode && !hasIntegratedDisclosures: {
|
24
|
+
const useNewDisclosure = await askUseNewDisclosure();
|
25
|
+
return useNewDisclosure ? DisclosureType.NEW : null;
|
26
|
+
}
|
27
|
+
case isIntegratedMode && hasIntegratedDisclosures:
|
28
|
+
return askDisclosureType();
|
29
|
+
case Boolean(options.disclosure):
|
30
|
+
return DisclosureType.EXISTING;
|
31
|
+
default:
|
32
|
+
return DisclosureType.NEW;
|
33
|
+
}
|
34
|
+
};
|
35
|
+
|
36
|
+
const runBatchIssuing = async (options) => {
|
37
|
+
validateDirectoryExists(options);
|
38
|
+
const fetchers = initFetchers(options);
|
39
|
+
const disclosures = await getIntegratedDisclosures(fetchers);
|
40
|
+
const disclosureType = await getDisclosureType(
|
41
|
+
!isEmpty(disclosures),
|
42
|
+
options
|
43
|
+
);
|
44
|
+
|
45
|
+
if (isEmpty(disclosureType)) {
|
46
|
+
return;
|
47
|
+
}
|
48
|
+
|
49
|
+
const { newDisclosureRequest, newExchangeOffers } = await prepareData(
|
50
|
+
disclosureType,
|
51
|
+
options
|
52
|
+
);
|
53
|
+
|
54
|
+
if (options.endpoint == null) {
|
55
|
+
printInfo('Dry Run would create:');
|
56
|
+
printInfo(
|
57
|
+
JSON.stringify(
|
58
|
+
omitBy(isNil, { newDisclosureRequest, newExchangeOffers }),
|
59
|
+
0,
|
60
|
+
2
|
61
|
+
)
|
62
|
+
);
|
63
|
+
return;
|
64
|
+
}
|
65
|
+
|
66
|
+
const disclosure = await createOrGotDisclosure({
|
67
|
+
disclosureType,
|
68
|
+
disclosures,
|
69
|
+
newDisclosureRequest,
|
70
|
+
fetchers,
|
71
|
+
options,
|
72
|
+
});
|
73
|
+
|
74
|
+
const disclosureRequest = await writeDisclosureToJson(disclosure, options);
|
75
|
+
|
76
|
+
const outputs = options.legacy
|
77
|
+
? await runLegacyBatchIssuing(
|
78
|
+
disclosureRequest,
|
79
|
+
newExchangeOffers,
|
80
|
+
fetchers,
|
81
|
+
options
|
82
|
+
)
|
83
|
+
: await runSingleQrCodeBatchIssuing(
|
84
|
+
disclosureRequest,
|
85
|
+
newExchangeOffers,
|
86
|
+
fetchers,
|
87
|
+
options
|
88
|
+
);
|
89
|
+
|
90
|
+
writeOutput(outputs, options);
|
91
|
+
};
|
92
|
+
|
93
|
+
const writeOutput = (outputs, options) => {
|
94
|
+
if (options.outputcsv && options.legacy) {
|
95
|
+
writeOutputCsv(outputs, options);
|
96
|
+
}
|
97
|
+
};
|
98
|
+
|
99
|
+
const runLegacyBatchIssuing = async (
|
100
|
+
disclosureRequest,
|
101
|
+
newExchangeOffers,
|
102
|
+
fetchers,
|
103
|
+
options
|
104
|
+
) => {
|
105
|
+
const outputs = [];
|
106
|
+
for (const newExchangeOffer of newExchangeOffers) {
|
107
|
+
outputs.push(
|
108
|
+
// eslint-disable-next-line no-await-in-loop
|
109
|
+
await createOfferExchangeAndQrCode({
|
110
|
+
...newExchangeOffer,
|
111
|
+
disclosureRequest,
|
112
|
+
fetchers,
|
113
|
+
options,
|
114
|
+
})
|
115
|
+
);
|
116
|
+
}
|
117
|
+
|
118
|
+
return outputs;
|
119
|
+
};
|
120
|
+
|
121
|
+
const runSingleQrCodeBatchIssuing = async (
|
122
|
+
disclosureRequest,
|
123
|
+
newExchangeOffers,
|
124
|
+
fetchers,
|
125
|
+
options
|
126
|
+
) => {
|
127
|
+
const outputs = [];
|
128
|
+
for (const newExchangeOffer of newExchangeOffers) {
|
129
|
+
outputs.push(
|
130
|
+
// eslint-disable-next-line no-await-in-loop
|
131
|
+
await createOfferExchange({
|
132
|
+
...newExchangeOffer,
|
133
|
+
disclosureRequest,
|
134
|
+
fetchers,
|
135
|
+
options,
|
136
|
+
})
|
137
|
+
);
|
138
|
+
}
|
139
|
+
|
140
|
+
printInfo('Generating qrcode & deep link');
|
141
|
+
const deeplink = await fetchers.loadDisclosureDeeplink(disclosureRequest);
|
142
|
+
printInfo(`Deep link: ${deeplink}`);
|
143
|
+
const qrcode = await fetchers.loadDisclosureQrcode(disclosureRequest);
|
144
|
+
const { filePath } = await writeQrCodeFile('qrcode-generic', qrcode, options);
|
145
|
+
printInfo(`QRCode saved: ${filePath}`);
|
146
|
+
|
147
|
+
return outputs;
|
148
|
+
};
|
149
|
+
|
150
|
+
const getIntegratedDisclosures = async (fetchers) => {
|
151
|
+
const vendorEndpoints = ['integrated-issuing-identification'];
|
152
|
+
const disclosures = await fetchers.getDisclosureList(vendorEndpoints);
|
153
|
+
return disclosures;
|
154
|
+
};
|
155
|
+
|
156
|
+
const getDisclosure = async (disclosures, fetchers, options) => {
|
157
|
+
const { disclosure } = options;
|
158
|
+
|
159
|
+
if (isString(disclosure)) {
|
160
|
+
return fetchers.getDisclosure(disclosure);
|
161
|
+
}
|
162
|
+
|
163
|
+
const disclosureId = await askDisclosureList(disclosures);
|
164
|
+
return find(({ id }) => id === disclosureId, disclosures);
|
165
|
+
};
|
166
|
+
|
167
|
+
const writeDisclosureToJson = async (disclosureRequest, options) => {
|
168
|
+
printInfo(`Using disclosureId:${disclosureRequest.id}`);
|
169
|
+
printInfo('');
|
170
|
+
|
171
|
+
const { filePath: disclosureFilePath } = await writeJsonFile(
|
172
|
+
disclosureRequest,
|
173
|
+
`disclosure-${disclosureRequest.id}`,
|
174
|
+
options
|
175
|
+
);
|
176
|
+
await writeJsonFile(
|
177
|
+
{
|
178
|
+
disclosureId: disclosureRequest.id,
|
179
|
+
disclosureFile: disclosureFilePath,
|
180
|
+
timestamp: new Date().toISOString(),
|
181
|
+
...options,
|
182
|
+
},
|
183
|
+
'lastrun',
|
184
|
+
options
|
185
|
+
);
|
186
|
+
return disclosureRequest;
|
187
|
+
};
|
188
|
+
|
189
|
+
const createOfferExchangeAndQrCode = async ({
|
190
|
+
newExchange,
|
191
|
+
newOffer,
|
192
|
+
disclosureRequest,
|
193
|
+
options,
|
194
|
+
fetchers,
|
195
|
+
}) => {
|
196
|
+
const { exchange, offer, vendorUserId } = await createOfferExchange({
|
197
|
+
newExchange,
|
198
|
+
newOffer,
|
199
|
+
disclosureRequest,
|
200
|
+
fetchers,
|
201
|
+
});
|
202
|
+
|
203
|
+
const deeplink = await fetchers.loadExchangeDeeplink(exchange);
|
204
|
+
const qrcode = await fetchers.loadExchangeQrcode(exchange);
|
205
|
+
const { filePath } = await writeQrCodeFile(
|
206
|
+
`qrcode-${vendorUserId}`,
|
207
|
+
qrcode,
|
208
|
+
options
|
209
|
+
);
|
210
|
+
printInfo(`${vendorUserId} Done. Qrcode file:${filePath}`);
|
211
|
+
printInfo('');
|
212
|
+
|
213
|
+
return {
|
214
|
+
exchange,
|
215
|
+
offer,
|
216
|
+
qrcode,
|
217
|
+
qrcodeFilePath: filePath,
|
218
|
+
deeplink,
|
219
|
+
vendorUserId,
|
220
|
+
};
|
221
|
+
};
|
222
|
+
|
223
|
+
const createOfferExchange = async ({
|
224
|
+
newExchange,
|
225
|
+
newOffer,
|
226
|
+
disclosureRequest,
|
227
|
+
fetchers,
|
228
|
+
}) => {
|
229
|
+
const { vendorUserId } = newOffer.credentialSubject;
|
230
|
+
printInfo(`Setting up vendorUserId:${vendorUserId}`);
|
231
|
+
const exchange = await fetchers.createOfferExchange({
|
232
|
+
...newExchange,
|
233
|
+
disclosureId: disclosureRequest.id,
|
234
|
+
});
|
235
|
+
const offer = await fetchers.createOffer(exchange, newOffer);
|
236
|
+
await fetchers.submitCompleteOffer(exchange, [offer]);
|
237
|
+
return {
|
238
|
+
exchange,
|
239
|
+
offer,
|
240
|
+
vendorUserId,
|
241
|
+
};
|
242
|
+
};
|
243
|
+
|
244
|
+
const createOrGotDisclosure = async ({
|
245
|
+
disclosureType,
|
246
|
+
disclosures,
|
247
|
+
newDisclosureRequest,
|
248
|
+
fetchers,
|
249
|
+
options,
|
250
|
+
}) => {
|
251
|
+
const disclosure =
|
252
|
+
disclosureType === DisclosureType.NEW
|
253
|
+
? await fetchers.createDisclosure(newDisclosureRequest)
|
254
|
+
: await getDisclosure(disclosures, fetchers, options);
|
255
|
+
return disclosure;
|
256
|
+
};
|
257
|
+
|
258
|
+
module.exports = { runBatchIssuing };
|
@@ -0,0 +1,208 @@
|
|
1
|
+
const { get, includes, isNil, map, omitBy, values } = require('lodash/fp');
|
2
|
+
const { formatISO } = require('date-fns/fp');
|
3
|
+
const { nanoid } = require('nanoid');
|
4
|
+
const {
|
5
|
+
loadHandlebarsTemplate,
|
6
|
+
loadCsvVariableSets,
|
7
|
+
computeActivationDate,
|
8
|
+
} = require('../helpers');
|
9
|
+
const { DisclosureType } = require('./constants');
|
10
|
+
|
11
|
+
const EMAIL_REGEX =
|
12
|
+
// eslint-disable-next-line max-len
|
13
|
+
/^(([^<>()[\]\\.,;:\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,}))$/;
|
14
|
+
|
15
|
+
const PHONE_REGEX = /\(?([0-9]{3})\)?([ .-]?)([0-9]{3})\2([0-9]{4})/;
|
16
|
+
const VENDOR_USER_ID_PATH = 'credentialSubject.vendorUserId';
|
17
|
+
|
18
|
+
const CREDENTIAL_TYPES = {
|
19
|
+
EMAIL: 'EmailV1.0',
|
20
|
+
PHONE: 'PhoneV1.0',
|
21
|
+
DRIVER_LICENSE: 'DriversLicenseV1.0',
|
22
|
+
ID_DOCUMENT: 'IdDocumentV1.0',
|
23
|
+
NATIONAL_ID_CARD: 'NationalIdCardV1.0',
|
24
|
+
PASSPORT: 'PassportV1.0',
|
25
|
+
PROOF_OF_AGE: 'ProofOfAgeV1.0',
|
26
|
+
RESIDENT_PERMIT: 'ResidentPermitV1.0',
|
27
|
+
};
|
28
|
+
|
29
|
+
const DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH =
|
30
|
+
'$.idDocumentCredentials[*].credentialSubject.identifier';
|
31
|
+
const credentialTypeToPick = {
|
32
|
+
[CREDENTIAL_TYPES.EMAIL]: '$.emails',
|
33
|
+
[CREDENTIAL_TYPES.PHONE]: '$.phones',
|
34
|
+
[CREDENTIAL_TYPES.DRIVER_LICENSE]: DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH,
|
35
|
+
[CREDENTIAL_TYPES.ID_DOCUMENT]: DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH,
|
36
|
+
[CREDENTIAL_TYPES.NATIONAL_ID_CARD]: DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH,
|
37
|
+
[CREDENTIAL_TYPES.PASSPORT]: DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH,
|
38
|
+
[CREDENTIAL_TYPES.PROOF_OF_AGE]: DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH,
|
39
|
+
[CREDENTIAL_TYPES.RESIDENT_PERMIT]: DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH,
|
40
|
+
};
|
41
|
+
|
42
|
+
const prepareData = async (
|
43
|
+
disclosureType,
|
44
|
+
{ csvFilename, offerTemplateFilename, vars: defaultVars, did, ...options }
|
45
|
+
) => {
|
46
|
+
validateCredentialType(options.credentialType);
|
47
|
+
const newDisclosureRequest = prepareNewDisclosureRequest(
|
48
|
+
disclosureType,
|
49
|
+
options
|
50
|
+
);
|
51
|
+
|
52
|
+
const variableSets = await loadCsvVariableSets(csvFilename, {
|
53
|
+
...defaultVars,
|
54
|
+
did,
|
55
|
+
});
|
56
|
+
const offerTemplate = loadHandlebarsTemplate(offerTemplateFilename);
|
57
|
+
const newExchangeOffers = map(
|
58
|
+
(variableSet) => ({
|
59
|
+
newOffer: prepareNewOffer({
|
60
|
+
template: offerTemplate,
|
61
|
+
variableSet,
|
62
|
+
...options,
|
63
|
+
}),
|
64
|
+
newExchange: prepareNewExchange({ variableSet, ...options }),
|
65
|
+
}),
|
66
|
+
variableSets
|
67
|
+
);
|
68
|
+
|
69
|
+
return { newDisclosureRequest, newExchangeOffers };
|
70
|
+
};
|
71
|
+
|
72
|
+
const prepareNewOffer = ({ template, variableSet, label }) => {
|
73
|
+
validateVariableSet(variableSet);
|
74
|
+
const offerString = template(variableSet);
|
75
|
+
const offer = JSON.parse(offerString);
|
76
|
+
validateVendorUserId(offer, variableSet);
|
77
|
+
return omitBy(isNil, {
|
78
|
+
...offer,
|
79
|
+
offerId: variableSet.offerId ?? nanoid(),
|
80
|
+
label,
|
81
|
+
});
|
82
|
+
};
|
83
|
+
|
84
|
+
const validateCredentialType = (credentialType) => {
|
85
|
+
const allowedCredentialTypes = values(CREDENTIAL_TYPES);
|
86
|
+
if (!credentialType || !allowedCredentialTypes.includes(credentialType)) {
|
87
|
+
throw new Error(
|
88
|
+
`${credentialType} doesn't exist. Please use one of ${allowedCredentialTypes}`
|
89
|
+
);
|
90
|
+
}
|
91
|
+
};
|
92
|
+
|
93
|
+
const validateVariableSet = (variableSet) => {
|
94
|
+
if (
|
95
|
+
variableSet.email == null &&
|
96
|
+
variableSet.phone == null &&
|
97
|
+
variableSet.identifier == null
|
98
|
+
) {
|
99
|
+
throw new Error('"email", "phone", or "identifier" column must be defined');
|
100
|
+
}
|
101
|
+
|
102
|
+
const { email, phone } = variableSet;
|
103
|
+
|
104
|
+
validateEmail(email);
|
105
|
+
validatePhone(phone);
|
106
|
+
};
|
107
|
+
|
108
|
+
const validateEmail = (email) => {
|
109
|
+
if (!email) return;
|
110
|
+
|
111
|
+
if (!EMAIL_REGEX.test(email)) {
|
112
|
+
throw new Error(`${email} is not a valid email`);
|
113
|
+
}
|
114
|
+
};
|
115
|
+
|
116
|
+
const validatePhone = (phone) => {
|
117
|
+
if (!phone) return;
|
118
|
+
|
119
|
+
if (!PHONE_REGEX.test(phone)) {
|
120
|
+
throw new Error(`${phone} is not a valid phone`);
|
121
|
+
}
|
122
|
+
};
|
123
|
+
|
124
|
+
const validateVendorUserId = (offer, variableSet) => {
|
125
|
+
const value = get(VENDOR_USER_ID_PATH, offer);
|
126
|
+
if (!includes(value, variableSet)) {
|
127
|
+
throw new Error(
|
128
|
+
`${VENDOR_USER_ID_PATH}: ${value} cannot be hardcoded and must be defined in ${JSON.stringify(
|
129
|
+
variableSet
|
130
|
+
)})`
|
131
|
+
);
|
132
|
+
}
|
133
|
+
};
|
134
|
+
|
135
|
+
const matcherToCredentialType = (credentialType, variableSet) => {
|
136
|
+
const typeToVariable = {
|
137
|
+
'EmailV1.0': variableSet.email,
|
138
|
+
'PhoneV1.0': variableSet.phone,
|
139
|
+
'DriversLicenseV1.0': variableSet.identifier,
|
140
|
+
'IdDocumentV1.0': variableSet.identifier,
|
141
|
+
'NationalIdCardV1.0': variableSet.identifier,
|
142
|
+
'PassportV1.0': variableSet.identifier,
|
143
|
+
'ProofOfAgeV1.0': variableSet.identifier,
|
144
|
+
'ResidentPermitV1.0': variableSet.identifier,
|
145
|
+
};
|
146
|
+
return typeToVariable[credentialType] || variableSet.email;
|
147
|
+
};
|
148
|
+
|
149
|
+
const prepareNewExchange = ({ variableSet, label, credentialType }) =>
|
150
|
+
omitBy(isNil, {
|
151
|
+
type: 'ISSUING',
|
152
|
+
identityMatcherValues: [
|
153
|
+
matcherToCredentialType(credentialType, variableSet),
|
154
|
+
],
|
155
|
+
label,
|
156
|
+
});
|
157
|
+
|
158
|
+
const prepareNewDisclosureRequest = (
|
159
|
+
disclosureType,
|
160
|
+
{
|
161
|
+
label,
|
162
|
+
termsUrl,
|
163
|
+
purpose,
|
164
|
+
credentialType,
|
165
|
+
authTokenExpiresIn = '10080',
|
166
|
+
...options
|
167
|
+
}
|
168
|
+
) => {
|
169
|
+
if (disclosureType !== DisclosureType.NEW) {
|
170
|
+
return undefined;
|
171
|
+
}
|
172
|
+
const activationDate = computeActivationDate(options);
|
173
|
+
|
174
|
+
const newDisclosureRequest = {
|
175
|
+
offerMode: 'preloaded',
|
176
|
+
configurationType: 'issuing',
|
177
|
+
vendorEndpoint: 'integrated-issuing-identification',
|
178
|
+
types: [
|
179
|
+
{
|
180
|
+
type: credentialType,
|
181
|
+
},
|
182
|
+
],
|
183
|
+
identityMatchers: {
|
184
|
+
rules: [
|
185
|
+
{
|
186
|
+
valueIndex: 0,
|
187
|
+
path: [credentialTypeToPick[credentialType]],
|
188
|
+
rule: 'pick',
|
189
|
+
},
|
190
|
+
],
|
191
|
+
vendorUserIdIndex: 0,
|
192
|
+
},
|
193
|
+
setIssuingDefault: true,
|
194
|
+
duration: '1h', // 1 hour by default
|
195
|
+
vendorDisclosureId: Date.now(),
|
196
|
+
purpose: purpose ?? 'Issuing Career Credential', // by default have a generic message
|
197
|
+
activationDate: formatISO(activationDate),
|
198
|
+
authTokenExpiresIn: Number(authTokenExpiresIn),
|
199
|
+
termsUrl,
|
200
|
+
label,
|
201
|
+
};
|
202
|
+
|
203
|
+
return omitBy(isNil, newDisclosureRequest);
|
204
|
+
};
|
205
|
+
|
206
|
+
module.exports = {
|
207
|
+
prepareData,
|
208
|
+
};
|
@@ -0,0 +1,47 @@
|
|
1
|
+
const inquirer = 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 inquirer.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,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);
|
@@ -0,0 +1,30 @@
|
|
1
|
+
const console = require('console');
|
2
|
+
const chalk = require('chalk');
|
3
|
+
const fs = require('fs');
|
4
|
+
const path = require('path');
|
5
|
+
|
6
|
+
const writeFile = (filePath, fileContent) => {
|
7
|
+
const fileBasename = path.basename(filePath, '.*');
|
8
|
+
|
9
|
+
console.info(`${chalk.green('Writing:')} ${chalk.whiteBright(fileBasename)}`);
|
10
|
+
|
11
|
+
fs.writeFileSync(filePath, fileContent, 'utf8');
|
12
|
+
};
|
13
|
+
|
14
|
+
const readFile = (filePath, missingError) => {
|
15
|
+
if (!fs.existsSync(filePath)) {
|
16
|
+
throw new Error(missingError);
|
17
|
+
}
|
18
|
+
|
19
|
+
return fs.readFileSync(filePath, 'utf8');
|
20
|
+
};
|
21
|
+
|
22
|
+
const printError = (ex) => console.error(ex);
|
23
|
+
const printInfo = (data) => console.info(data);
|
24
|
+
|
25
|
+
module.exports = {
|
26
|
+
printInfo,
|
27
|
+
writeFile,
|
28
|
+
readFile,
|
29
|
+
printError,
|
30
|
+
};
|
@@ -0,0 +1,12 @@
|
|
1
|
+
const { flow } = require('lodash/fp');
|
2
|
+
const { addHours } = require('date-fns/fp');
|
3
|
+
|
4
|
+
const computeActivationDate = ({
|
5
|
+
activatesInHours = 0 /* by default activate immediately */,
|
6
|
+
}) => {
|
7
|
+
return flow(addHours(activatesInHours))(new Date());
|
8
|
+
};
|
9
|
+
|
10
|
+
module.exports = {
|
11
|
+
computeActivationDate,
|
12
|
+
};
|