@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.
- package/LICENSE +201 -0
- package/jest.config.js +20 -0
- package/package.json +45 -0
- package/src/batch-issuing/README.md +97 -0
- package/src/batch-issuing/constants.js +33 -0
- package/src/batch-issuing/fetchers.js +114 -0
- package/src/batch-issuing/file-readers.js +15 -0
- package/src/batch-issuing/file-writers.js +34 -0
- package/src/batch-issuing/index.js +124 -0
- package/src/batch-issuing/orchestrators.js +332 -0
- package/src/batch-issuing/prepare-data.js +167 -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 +8 -0
- package/src/helpers/load-csv.js +34 -0
- package/src/helpers/load-handlebars-template.js +9 -0
- package/src/helpers/parse-column.js +8 -0
- package/src/helpers/prepare-variable-sets.js +23 -0
- package/src/index.js +3 -0
- package/src/vendor-credentials/README.md +72 -0
- package/src/vendor-credentials/execute-update.js +32 -0
- package/src/vendor-credentials/index.js +49 -0
- package/src/vendor-credentials/orchestrator.js +22 -0
- package/src/vendor-credentials/prepare-data.js +36 -0
- package/test/batch-issuing.test.js +1523 -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/data/with-bom.csv +3 -0
- 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);
|