@rockcarver/frodo-lib 0.11.0

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 (112) hide show
  1. package/.eslintrc +32 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +30 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  4. package/.github/README.md +121 -0
  5. package/.github/workflows/pipeline.yml +287 -0
  6. package/.prettierrc +6 -0
  7. package/CHANGELOG.md +512 -0
  8. package/CODE_OF_CONDUCT.md +128 -0
  9. package/LICENSE +21 -0
  10. package/README.md +8 -0
  11. package/docs/CONTRIBUTE.md +96 -0
  12. package/docs/PIPELINE.md +169 -0
  13. package/docs/images/npm_versioning_guidelines.png +0 -0
  14. package/docs/images/release_pipeline.png +0 -0
  15. package/jsconfig.json +6 -0
  16. package/package.json +95 -0
  17. package/resources/sampleEntitiesFile.json +8 -0
  18. package/resources/sampleEnvFile.env +2 -0
  19. package/src/api/AuthenticateApi.js +33 -0
  20. package/src/api/BaseApi.js +242 -0
  21. package/src/api/CirclesOfTrustApi.js +87 -0
  22. package/src/api/EmailTemplateApi.js +37 -0
  23. package/src/api/IdmConfigApi.js +88 -0
  24. package/src/api/LogApi.js +45 -0
  25. package/src/api/ManagedObjectApi.js +62 -0
  26. package/src/api/OAuth2ClientApi.js +69 -0
  27. package/src/api/OAuth2OIDCApi.js +73 -0
  28. package/src/api/OAuth2ProviderApi.js +32 -0
  29. package/src/api/RealmApi.js +99 -0
  30. package/src/api/Saml2Api.js +176 -0
  31. package/src/api/ScriptApi.js +84 -0
  32. package/src/api/SecretsApi.js +151 -0
  33. package/src/api/ServerInfoApi.js +41 -0
  34. package/src/api/SocialIdentityProvidersApi.js +114 -0
  35. package/src/api/StartupApi.js +45 -0
  36. package/src/api/ThemeApi.js +181 -0
  37. package/src/api/TreeApi.js +207 -0
  38. package/src/api/VariablesApi.js +104 -0
  39. package/src/api/utils/ApiUtils.js +77 -0
  40. package/src/api/utils/ApiUtils.test.js +96 -0
  41. package/src/api/utils/Base64.js +62 -0
  42. package/src/index.js +32 -0
  43. package/src/index.test.js +13 -0
  44. package/src/ops/AdminOps.js +901 -0
  45. package/src/ops/AuthenticateOps.js +342 -0
  46. package/src/ops/CirclesOfTrustOps.js +350 -0
  47. package/src/ops/ConnectionProfileOps.js +254 -0
  48. package/src/ops/EmailTemplateOps.js +326 -0
  49. package/src/ops/IdmOps.js +227 -0
  50. package/src/ops/IdpOps.js +342 -0
  51. package/src/ops/JourneyOps.js +2026 -0
  52. package/src/ops/LogOps.js +357 -0
  53. package/src/ops/ManagedObjectOps.js +34 -0
  54. package/src/ops/OAuth2ClientOps.js +151 -0
  55. package/src/ops/OrganizationOps.js +85 -0
  56. package/src/ops/RealmOps.js +139 -0
  57. package/src/ops/SamlOps.js +541 -0
  58. package/src/ops/ScriptOps.js +211 -0
  59. package/src/ops/SecretsOps.js +288 -0
  60. package/src/ops/StartupOps.js +114 -0
  61. package/src/ops/ThemeOps.js +379 -0
  62. package/src/ops/VariablesOps.js +185 -0
  63. package/src/ops/templates/OAuth2ClientTemplate.json +270 -0
  64. package/src/ops/templates/OrgModelUserAttributesTemplate.json +149 -0
  65. package/src/ops/templates/cloud/GenericExtensionAttributesTemplate.json +392 -0
  66. package/src/ops/templates/cloud/managed.json +4119 -0
  67. package/src/ops/utils/Console.js +434 -0
  68. package/src/ops/utils/DataProtection.js +92 -0
  69. package/src/ops/utils/DataProtection.test.js +28 -0
  70. package/src/ops/utils/ExportImportUtils.js +146 -0
  71. package/src/ops/utils/ExportImportUtils.test.js +119 -0
  72. package/src/ops/utils/OpsUtils.js +76 -0
  73. package/src/ops/utils/Wordwrap.js +11 -0
  74. package/src/storage/SessionStorage.js +45 -0
  75. package/src/storage/StaticStorage.js +15 -0
  76. package/test/e2e/journey/baseline/ForgottenUsername.journey.json +216 -0
  77. package/test/e2e/journey/baseline/Login.journey.json +205 -0
  78. package/test/e2e/journey/baseline/PasswordGrant.journey.json +139 -0
  79. package/test/e2e/journey/baseline/ProgressiveProfile.journey.json +198 -0
  80. package/test/e2e/journey/baseline/Registration.journey.json +249 -0
  81. package/test/e2e/journey/baseline/ResetPassword.journey.json +268 -0
  82. package/test/e2e/journey/baseline/UpdatePassword.journey.json +323 -0
  83. package/test/e2e/journey/baseline/allAlphaJourneys.journeys.json +1520 -0
  84. package/test/e2e/journey/delete/ForgottenUsername.journey.json +216 -0
  85. package/test/e2e/journey/delete/Login.journey.json +205 -0
  86. package/test/e2e/journey/delete/PasswordGrant.journey.json +139 -0
  87. package/test/e2e/journey/delete/ProgressiveProfile.journey.json +198 -0
  88. package/test/e2e/journey/delete/Registration.journey.json +249 -0
  89. package/test/e2e/journey/delete/ResetPassword.journey.json +268 -0
  90. package/test/e2e/journey/delete/UpdatePassword.journey.json +323 -0
  91. package/test/e2e/journey/delete/deleteMe.journey.json +230 -0
  92. package/test/e2e/journey/list/Disabled.journey.json +43 -0
  93. package/test/e2e/journey/list/ForgottenUsername.journey.json +216 -0
  94. package/test/e2e/journey/list/Login.journey.json +205 -0
  95. package/test/e2e/journey/list/PasswordGrant.journey.json +139 -0
  96. package/test/e2e/journey/list/ProgressiveProfile.journey.json +198 -0
  97. package/test/e2e/journey/list/Registration.journey.json +249 -0
  98. package/test/e2e/journey/list/ResetPassword.journey.json +268 -0
  99. package/test/e2e/journey/list/UpdatePassword.journey.json +323 -0
  100. package/test/e2e/setup.js +107 -0
  101. package/test/e2e/theme/baseline/Contrast.theme.json +95 -0
  102. package/test/e2e/theme/baseline/Highlander.theme.json +95 -0
  103. package/test/e2e/theme/baseline/Robroy.theme.json +95 -0
  104. package/test/e2e/theme/baseline/Starter-Theme.theme.json +94 -0
  105. package/test/e2e/theme/baseline/Zardoz.theme.json +95 -0
  106. package/test/e2e/theme/import/Contrast.theme.json +95 -0
  107. package/test/e2e/theme/import/Highlander.theme.json +95 -0
  108. package/test/e2e/theme/import/Robroy.theme.json +95 -0
  109. package/test/e2e/theme/import/Starter-Theme.theme.json +94 -0
  110. package/test/e2e/theme/import/Zardoz.default.theme.json +95 -0
  111. package/test/fs_tmp/.gitkeep +2 -0
  112. package/test/global/setup.js +65 -0
@@ -0,0 +1,2026 @@
1
+ /* eslint-disable no-param-reassign */
2
+ import fs from 'fs';
3
+ import yesno from 'yesno';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+ import _ from 'lodash';
6
+ import {
7
+ convertBase64TextToArray,
8
+ getTypedFilename,
9
+ saveJsonToFile,
10
+ getRealmString,
11
+ convertTextArrayToBase64,
12
+ convertTextArrayToBase64Url,
13
+ } from './utils/ExportImportUtils.js';
14
+ import { getRealmManagedUser, replaceAll } from './utils/OpsUtils.js';
15
+ import storage from '../storage/SessionStorage.js';
16
+ import {
17
+ getNode,
18
+ putNode,
19
+ deleteNode,
20
+ getTrees,
21
+ getTree,
22
+ putTree,
23
+ getNodeTypes,
24
+ getNodesByType,
25
+ deleteTree,
26
+ } from '../api/TreeApi.js';
27
+ import { getEmailTemplate, putEmailTemplate } from '../api/EmailTemplateApi.js';
28
+ import { getScript } from '../api/ScriptApi.js';
29
+ import * as global from '../storage/StaticStorage.js';
30
+ import {
31
+ printMessage,
32
+ createProgressBar,
33
+ updateProgressBar,
34
+ stopProgressBar,
35
+ showSpinner,
36
+ succeedSpinner,
37
+ createTable,
38
+ spinSpinner,
39
+ failSpinner,
40
+ stopSpinner,
41
+ warnSpinner,
42
+ } from './utils/Console.js';
43
+ import wordwrap from './utils/Wordwrap.js';
44
+ import {
45
+ getProviderByLocationAndId,
46
+ getProviders,
47
+ getProviderMetadata,
48
+ createProvider,
49
+ findProviders,
50
+ updateProvider,
51
+ } from '../api/Saml2Api.js';
52
+ import {
53
+ createCircleOfTrust,
54
+ getCirclesOfTrust,
55
+ updateCircleOfTrust,
56
+ } from '../api/CirclesOfTrustApi.js';
57
+ import {
58
+ decode,
59
+ encode,
60
+ encodeBase64Url,
61
+ isBase64Encoded,
62
+ } from '../api/utils/Base64.js';
63
+ import {
64
+ getSocialIdentityProviders,
65
+ putProviderByTypeAndId,
66
+ } from '../api/SocialIdentityProvidersApi.js';
67
+ import { getThemes, putThemes } from '../api/ThemeApi.js';
68
+ import { createOrUpdateScript } from './ScriptOps.js';
69
+
70
+ const containerNodes = ['PageNode', 'CustomPageNode'];
71
+
72
+ const scriptedNodes = [
73
+ 'ConfigProviderNode',
74
+ 'ScriptedDecisionNode',
75
+ 'ClientScriptNode',
76
+ 'SocialProviderHandlerNode',
77
+ 'CustomScriptNode',
78
+ ];
79
+
80
+ const emailTemplateNodes = ['EmailSuspendNode', 'EmailTemplateNode'];
81
+
82
+ // use a function vs a template variable to avoid problems in loops
83
+ function getSingleTreeFileDataTemplate() {
84
+ return {
85
+ meta: {},
86
+ innerNodes: {},
87
+ nodes: {},
88
+ scripts: {},
89
+ emailTemplates: {},
90
+ socialIdentityProviders: {},
91
+ themes: [],
92
+ saml2Entities: {},
93
+ circlesOfTrust: {},
94
+ tree: {},
95
+ };
96
+ }
97
+
98
+ // use a function vs a template variable to avoid problems in loops
99
+ function getMultipleTreesFileDataTemplate() {
100
+ return {
101
+ meta: {},
102
+ trees: {},
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Helper to get all SAML2 dependencies for a given node object
108
+ * @param {Object} nodeObject node object
109
+ * @param {[Object]} allProviders array of all saml2 providers objects
110
+ * @param {[Object]} allCirclesOfTrust array of all circle of trust objects
111
+ * @returns {Promise} a promise that resolves to an object containing a saml2 dependencies
112
+ */
113
+ async function getSaml2NodeDependencies(
114
+ nodeObject,
115
+ allProviders,
116
+ allCirclesOfTrust
117
+ ) {
118
+ const samlProperties = ['metaAlias', 'idpEntityId'];
119
+ const saml2EntityPromises = [];
120
+ samlProperties.forEach(async (samlProperty) => {
121
+ // In the following line nodeObject[samlProperty] will look like '/alpha/iSPAzure'.
122
+ const entityId =
123
+ samlProperty === 'metaAlias'
124
+ ? _.last(nodeObject[samlProperty].split('/'))
125
+ : nodeObject[samlProperty];
126
+ const entity = _.find(allProviders, { entityId });
127
+ if (entity) {
128
+ saml2EntityPromises.push(
129
+ getProviderByLocationAndId(entity.location, entity._id).then(
130
+ (providerResponse) => {
131
+ /**
132
+ * Adding entityLocation here to the entityResponse because the import tool
133
+ * needs to know whether the saml2 entity is remote or not (this will be removed
134
+ * from the config before importing see updateSaml2Entity and createSaml2Entity functions).
135
+ * Importing a remote saml2 entity is a slightly different request (see createSaml2Entity).
136
+ */
137
+ providerResponse.data.entityLocation = entity.location;
138
+
139
+ if (entity.location === 'remote') {
140
+ // get the xml representation of this entity and add it to the entityResponse;
141
+ return getProviderMetadata(providerResponse.data.entityId).then(
142
+ (metaDataResponse) => {
143
+ providerResponse.data.base64EntityXML = encodeBase64Url(
144
+ metaDataResponse.data
145
+ );
146
+ return providerResponse;
147
+ }
148
+ );
149
+ }
150
+ return providerResponse;
151
+ }
152
+ )
153
+ );
154
+ }
155
+ });
156
+ return Promise.all(saml2EntityPromises).then(
157
+ (saml2EntitiesPromisesResults) => {
158
+ const saml2Entities = [];
159
+ saml2EntitiesPromisesResults.forEach((saml2Entity) => {
160
+ if (saml2Entity) {
161
+ saml2Entities.push(saml2Entity.data);
162
+ }
163
+ });
164
+ const samlEntityIds = _.map(
165
+ saml2Entities,
166
+ (saml2EntityConfig) => `${saml2EntityConfig.entityId}|saml2`
167
+ );
168
+ const circlesOfTrust = _.filter(allCirclesOfTrust, (circleOfTrust) => {
169
+ let hasEntityId = false;
170
+ circleOfTrust.trustedProviders.forEach((trustedProvider) => {
171
+ if (!hasEntityId && samlEntityIds.includes(trustedProvider)) {
172
+ hasEntityId = true;
173
+ }
174
+ });
175
+ return hasEntityId;
176
+ });
177
+ const saml2NodeDependencies = {
178
+ saml2Entities,
179
+ circlesOfTrust,
180
+ };
181
+ return saml2NodeDependencies;
182
+ }
183
+ );
184
+ }
185
+
186
+ /**
187
+ * Helper method to create export data for a tree with all its
188
+ * dependencies. The export data can be written to a file as is
189
+ * (but it doesn't contain meta data).
190
+ * @param {Object} treeObject tree object
191
+ * @param {Object} exportData export data
192
+ * @param {Object} options options object
193
+ */
194
+ async function exportTree(treeObject, exportData, options) {
195
+ const { useStringArrays } = options;
196
+ const { deps } = options;
197
+ const { verbose } = options;
198
+
199
+ if (verbose) printMessage(`\n- ${treeObject._id}\n`, 'info', false);
200
+
201
+ // Process tree
202
+ if (verbose) printMessage(' - Flow');
203
+ exportData.tree = treeObject;
204
+ if (verbose && treeObject.identityResource)
205
+ printMessage(
206
+ ` - identityResource: ${treeObject.identityResource}`,
207
+ 'info'
208
+ );
209
+ if (verbose) printMessage(` - Done`, 'info');
210
+
211
+ const nodePromises = [];
212
+ const scriptPromises = [];
213
+ const emailTemplatePromises = [];
214
+ const innerNodePromises = [];
215
+ const saml2ConfigPromises = [];
216
+ let socialProviderPromise = null;
217
+ const themePromise =
218
+ deps &&
219
+ storage.session.getDeploymentType() !== global.CLASSIC_DEPLOYMENT_TYPE_KEY
220
+ ? getThemes().catch((error) => {
221
+ printMessage(error, 'error');
222
+ })
223
+ : null;
224
+
225
+ let allSaml2Providers = null;
226
+ let allCirclesOfTrust = null;
227
+ let filteredSocialProviders = null;
228
+ const themes = [];
229
+
230
+ // get all the nodes
231
+ for (const [nodeId, nodeInfo] of Object.entries(treeObject.nodes)) {
232
+ nodePromises.push(
233
+ getNode(nodeId, nodeInfo.nodeType).then((response) => response.data)
234
+ );
235
+ }
236
+ if (verbose && nodePromises.length > 0) printMessage(' - Nodes:');
237
+ const nodeObjects = await Promise.all(nodePromises);
238
+
239
+ // iterate over every node in tree
240
+ for (const nodeObject of nodeObjects) {
241
+ const nodeId = nodeObject._id;
242
+ const nodeType = nodeObject._type._id;
243
+ if (verbose) printMessage(` - ${nodeId} (${nodeType})`, 'info', true);
244
+ exportData.nodes[nodeObject._id] = nodeObject;
245
+
246
+ // handle script node types
247
+ if (deps && scriptedNodes.includes(nodeType)) {
248
+ scriptPromises.push(getScript(nodeObject.script));
249
+ }
250
+
251
+ // frodo supports email templates in platform deployments
252
+ if (
253
+ (deps &&
254
+ storage.session.getDeploymentType() ===
255
+ global.CLOUD_DEPLOYMENT_TYPE_KEY) ||
256
+ storage.session.getDeploymentType() ===
257
+ global.FORGEOPS_DEPLOYMENT_TYPE_KEY
258
+ ) {
259
+ if (emailTemplateNodes.includes(nodeType)) {
260
+ emailTemplatePromises.push(
261
+ getEmailTemplate(nodeObject.emailTemplateName).catch((error) => {
262
+ let message = `${error}`;
263
+ if (error.isAxiosError && error.response.status) {
264
+ message = error.response.statusText;
265
+ }
266
+ printMessage(
267
+ `\n${message}: Email Template "${nodeObject.emailTemplateName}"`,
268
+ 'error'
269
+ );
270
+ })
271
+ );
272
+ }
273
+ }
274
+
275
+ // handle SAML2 node dependencies
276
+ if (deps && nodeType === 'product-Saml2Node') {
277
+ if (!allSaml2Providers) {
278
+ // eslint-disable-next-line no-await-in-loop
279
+ allSaml2Providers = (await getProviders()).data.result;
280
+ }
281
+ if (!allCirclesOfTrust) {
282
+ // eslint-disable-next-line no-await-in-loop
283
+ allCirclesOfTrust = (await getCirclesOfTrust()).data.result;
284
+ }
285
+ saml2ConfigPromises.push(
286
+ getSaml2NodeDependencies(
287
+ nodeObject,
288
+ allSaml2Providers,
289
+ allCirclesOfTrust
290
+ )
291
+ );
292
+ }
293
+
294
+ // If this is a SocialProviderHandlerNode get each enabled social identity provider.
295
+ if (
296
+ deps &&
297
+ !socialProviderPromise &&
298
+ nodeType === 'SocialProviderHandlerNode'
299
+ ) {
300
+ socialProviderPromise = getSocialIdentityProviders();
301
+ }
302
+
303
+ // If this is a SelectIdPNode and filteredProviters is not already set to empty array set filteredSocialProviers.
304
+ if (deps && !filteredSocialProviders && nodeType === 'SelectIdPNode') {
305
+ filteredSocialProviders = filteredSocialProviders || [];
306
+ for (const filteredProvider of nodeObject.filteredProviders) {
307
+ if (!filteredSocialProviders.includes(filteredProvider)) {
308
+ filteredSocialProviders.push(filteredProvider);
309
+ }
310
+ }
311
+ }
312
+
313
+ // get inner nodes (nodes inside container nodes)
314
+ if (containerNodes.includes(nodeType)) {
315
+ for (const innerNode of nodeObject.nodes) {
316
+ innerNodePromises.push(
317
+ getNode(innerNode._id, innerNode.nodeType).then(
318
+ (response) => response.data
319
+ )
320
+ );
321
+ }
322
+ // frodo supports themes in platform deployments
323
+ if (
324
+ (deps &&
325
+ storage.session.getDeploymentType() ===
326
+ global.CLOUD_DEPLOYMENT_TYPE_KEY) ||
327
+ storage.session.getDeploymentType() ===
328
+ global.FORGEOPS_DEPLOYMENT_TYPE_KEY
329
+ ) {
330
+ let themeId = false;
331
+
332
+ if (nodeObject.stage) {
333
+ // see if themeId is part of the stage object
334
+ try {
335
+ themeId = JSON.parse(nodeObject.stage).themeId;
336
+ } catch (e) {
337
+ themeId = false;
338
+ }
339
+ // if the page node's themeId is set the "old way" set themeId accordingly
340
+ if (!themeId && nodeObject.stage.indexOf('themeId=') === 0) {
341
+ // eslint-disable-next-line prefer-destructuring
342
+ themeId = nodeObject.stage.split('=')[1];
343
+ }
344
+ }
345
+
346
+ if (themeId) {
347
+ if (!themes.includes(themeId)) themes.push(themeId);
348
+ }
349
+ }
350
+ }
351
+ }
352
+
353
+ // Process inner nodes
354
+ if (verbose && innerNodePromises.length > 0) printMessage(' - Inner nodes:');
355
+ const innerNodeDataResults = await Promise.all(innerNodePromises);
356
+ for (const innerNodeObject of innerNodeDataResults) {
357
+ const innerNodeId = innerNodeObject._id;
358
+ const innerNodeType = innerNodeObject._type._id;
359
+ if (verbose)
360
+ printMessage(` - ${innerNodeId} (${innerNodeType})`, 'info', true);
361
+ exportData.innerNodes[innerNodeId] = innerNodeObject;
362
+
363
+ // handle script node types
364
+ if (deps && scriptedNodes.includes(innerNodeType)) {
365
+ scriptPromises.push(getScript(innerNodeObject.script));
366
+ }
367
+
368
+ // frodo supports email templates in platform deployments
369
+ if (
370
+ (deps &&
371
+ storage.session.getDeploymentType() ===
372
+ global.CLOUD_DEPLOYMENT_TYPE_KEY) ||
373
+ storage.session.getDeploymentType() ===
374
+ global.FORGEOPS_DEPLOYMENT_TYPE_KEY
375
+ ) {
376
+ if (emailTemplateNodes.includes(innerNodeType)) {
377
+ emailTemplatePromises.push(
378
+ getEmailTemplate(innerNodeObject.emailTemplateName).catch((error) => {
379
+ let message = `${error}`;
380
+ if (error.isAxiosError && error.response.status) {
381
+ message = error.response.statusText;
382
+ }
383
+ printMessage(
384
+ `\n${message}: Email Template "${innerNodeObject.emailTemplateName}"`,
385
+ 'error'
386
+ );
387
+ })
388
+ );
389
+ }
390
+ }
391
+
392
+ // handle SAML2 node dependencies
393
+ if (deps && innerNodeType === 'product-Saml2Node') {
394
+ printMessage('SAML2 inner node', 'error');
395
+ if (!allSaml2Providers) {
396
+ // eslint-disable-next-line no-await-in-loop
397
+ allSaml2Providers = (await getProviders()).data.result;
398
+ }
399
+ if (!allCirclesOfTrust) {
400
+ // eslint-disable-next-line no-await-in-loop
401
+ allCirclesOfTrust = (await getCirclesOfTrust()).data.result;
402
+ }
403
+ saml2ConfigPromises.push(
404
+ getSaml2NodeDependencies(
405
+ innerNodeObject,
406
+ allSaml2Providers,
407
+ allCirclesOfTrust
408
+ )
409
+ );
410
+ }
411
+
412
+ // If this is a SocialProviderHandlerNode get each enabled social identity provider.
413
+ if (
414
+ deps &&
415
+ !socialProviderPromise &&
416
+ innerNodeType === 'SocialProviderHandlerNode'
417
+ ) {
418
+ socialProviderPromise = getSocialIdentityProviders();
419
+ }
420
+
421
+ // If this is a SelectIdPNode and filteredProviters is not already set to empty array set filteredSocialProviers.
422
+ if (
423
+ deps &&
424
+ !filteredSocialProviders &&
425
+ innerNodeType === 'SelectIdPNode' &&
426
+ innerNodeObject.filteredProviders
427
+ ) {
428
+ filteredSocialProviders = filteredSocialProviders || [];
429
+ for (const filteredProvider of innerNodeObject.filteredProviders) {
430
+ if (!filteredSocialProviders.includes(filteredProvider)) {
431
+ filteredSocialProviders.push(filteredProvider);
432
+ }
433
+ }
434
+ }
435
+ }
436
+
437
+ // Process email templates
438
+ if (verbose && emailTemplatePromises.length > 0)
439
+ printMessage(' - Email templates:');
440
+ const settledEmailTemplatePromises = await Promise.allSettled(
441
+ emailTemplatePromises
442
+ );
443
+ settledEmailTemplatePromises.forEach((settledPromise) => {
444
+ if (settledPromise.status === 'fulfilled' && settledPromise.value) {
445
+ if (verbose)
446
+ printMessage(
447
+ ` - ${settledPromise.value.data._id.split('/')[1]}${
448
+ settledPromise.value.data.displayName
449
+ ? ` (${settledPromise.value.data.displayName})`
450
+ : ''
451
+ }`,
452
+ 'info',
453
+ true
454
+ );
455
+ exportData.emailTemplates[settledPromise.value.data._id.split('/')[1]] =
456
+ settledPromise.value.data;
457
+ }
458
+ });
459
+
460
+ // Process SAML2 providers and circles of trust
461
+ const saml2NodeDependencies = await Promise.all(saml2ConfigPromises);
462
+ saml2NodeDependencies.forEach((saml2NodeDependency) => {
463
+ if (saml2NodeDependency) {
464
+ if (verbose) printMessage(' - SAML2 entity providers:');
465
+ saml2NodeDependency.saml2Entities.forEach((saml2Entity) => {
466
+ if (verbose)
467
+ printMessage(
468
+ ` - ${saml2Entity.entityLocation} ${saml2Entity.entityId}`,
469
+ 'info'
470
+ );
471
+ exportData.saml2Entities[saml2Entity._id] = saml2Entity;
472
+ });
473
+ if (verbose) printMessage(' - SAML2 circles of trust:');
474
+ saml2NodeDependency.circlesOfTrust.forEach((circleOfTrust) => {
475
+ if (verbose) printMessage(` - ${circleOfTrust._id}`, 'info');
476
+ exportData.circlesOfTrust[circleOfTrust._id] = circleOfTrust;
477
+ });
478
+ }
479
+ });
480
+
481
+ // Process socialIdentityProviders
482
+ const socialProvidersResponse = await Promise.resolve(socialProviderPromise);
483
+ if (socialProvidersResponse) {
484
+ if (verbose) printMessage(' - OAuth2/OIDC (social) identity providers:');
485
+ socialProvidersResponse.data.result.forEach((socialProvider) => {
486
+ // If the list of socialIdentityProviders needs to be filtered based on the
487
+ // filteredProviders property of a SelectIdPNode do it here.
488
+ if (
489
+ socialProvider &&
490
+ (!filteredSocialProviders ||
491
+ filteredSocialProviders.length === 0 ||
492
+ filteredSocialProviders.includes(socialProvider._id))
493
+ ) {
494
+ if (verbose) printMessage(` - ${socialProvider._id}`, 'info');
495
+ scriptPromises.push(getScript(socialProvider.transform));
496
+ exportData.socialIdentityProviders[socialProvider._id] = socialProvider;
497
+ }
498
+ });
499
+ }
500
+
501
+ // Process scripts
502
+ if (verbose && scriptPromises.length > 0) printMessage(' - Scripts:');
503
+ const scripts = await Promise.all(scriptPromises);
504
+ scripts.forEach((scriptResultObject) => {
505
+ const scriptObject = _.get(scriptResultObject, 'data');
506
+ if (scriptObject) {
507
+ if (verbose)
508
+ printMessage(
509
+ ` - ${scriptObject._id} (${scriptObject.name})`,
510
+ 'info',
511
+ true
512
+ );
513
+ if (useStringArrays) {
514
+ scriptObject.script = convertBase64TextToArray(scriptObject.script);
515
+ } else {
516
+ scriptObject.script = JSON.stringify(decode(scriptObject.script));
517
+ }
518
+ exportData.scripts[scriptObject._id] = scriptObject;
519
+ }
520
+ });
521
+
522
+ // Process themes
523
+ if (themePromise) {
524
+ if (verbose) printMessage(' - Themes:');
525
+ await Promise.resolve(themePromise).then((themePromiseResults) => {
526
+ themePromiseResults.forEach((themeObject) => {
527
+ if (
528
+ themeObject &&
529
+ // has the theme been specified by id or name in a page node?
530
+ (themes.includes(themeObject._id) ||
531
+ themes.includes(themeObject.name) ||
532
+ // has this journey been linked to a theme?
533
+ themeObject.linkedTrees.includes(treeObject._id))
534
+ ) {
535
+ if (verbose)
536
+ printMessage(
537
+ ` - ${themeObject._id} (${themeObject.name})`,
538
+ 'info'
539
+ );
540
+ exportData.themes.push(themeObject);
541
+ }
542
+ });
543
+ });
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Export journey by id/name to file
549
+ * @param {String} journeyId journey id/name
550
+ * @param {String} file optional export file name
551
+ * @param {Object} options reUuid:boolean: re-uuid all node objects, verbose:boolean: verbose output, deps:boolean: include dependencies
552
+ */
553
+ export async function exportJourneyToFile(journeyId, file, options) {
554
+ const { verbose } = options;
555
+ let fileName = file;
556
+ if (!fileName) {
557
+ fileName = getTypedFilename(journeyId, 'journey');
558
+ }
559
+ if (!verbose) showSpinner(`${journeyId}`);
560
+ getTree(journeyId)
561
+ .then(async (response) => {
562
+ const treeData = response.data;
563
+ const fileData = getSingleTreeFileDataTemplate();
564
+ try {
565
+ await exportTree(treeData, fileData, options);
566
+ if (verbose) showSpinner(`${journeyId}`);
567
+ saveJsonToFile(fileData, fileName);
568
+ succeedSpinner(
569
+ `Exported ${journeyId.brightCyan} to ${fileName.brightCyan}.`
570
+ );
571
+ } catch (error) {
572
+ if (verbose) showSpinner(`${journeyId}`);
573
+ failSpinner(`Error exporting journey ${journeyId}: ${error}`);
574
+ }
575
+ })
576
+ .catch((err) => {
577
+ failSpinner(err.message);
578
+ });
579
+ }
580
+
581
+ /**
582
+ * Export all journeys to file
583
+ * @param {String} file optional export file name
584
+ */
585
+ export async function exportJourneysToFile(file, options) {
586
+ let fileName = file;
587
+ if (!fileName) {
588
+ fileName = getTypedFilename(`all${getRealmString()}Journeys`, 'journeys');
589
+ }
590
+ const trees = (await getTrees()).data.result;
591
+ const fileData = getMultipleTreesFileDataTemplate();
592
+ createProgressBar(trees.length, 'Exporting journeys...');
593
+ for (const tree of trees) {
594
+ updateProgressBar(`${tree._id}`);
595
+ try {
596
+ // eslint-disable-next-line no-await-in-loop
597
+ const treeData = (await getTree(tree._id)).data;
598
+ const exportData = getSingleTreeFileDataTemplate();
599
+ delete exportData.meta;
600
+ // eslint-disable-next-line no-await-in-loop
601
+ await exportTree(treeData, exportData, options);
602
+ fileData.trees[tree._id] = exportData;
603
+ } catch (error) {
604
+ printMessage(`Error exporting journey ${tree._id}: ${error}`, 'error');
605
+ }
606
+ }
607
+ saveJsonToFile(fileData, fileName);
608
+ stopProgressBar(`Exported to ${fileName}`);
609
+ }
610
+
611
+ /**
612
+ * Export all journeys to separate files
613
+ */
614
+ export async function exportJourneysToFiles(options) {
615
+ const trees = (await getTrees()).data.result;
616
+ createProgressBar(trees.length, 'Exporting journeys...');
617
+ for (const tree of trees) {
618
+ updateProgressBar(`${tree._id}`);
619
+ const fileName = getTypedFilename(`${tree._id}`, 'journey');
620
+ // eslint-disable-next-line no-await-in-loop
621
+ const treeData = (await getTree(tree._id)).data;
622
+ const exportData = getSingleTreeFileDataTemplate();
623
+ // eslint-disable-next-line no-await-in-loop
624
+ await exportTree(treeData, exportData, options);
625
+ saveJsonToFile(exportData, fileName);
626
+ }
627
+ stopProgressBar('Done');
628
+ }
629
+
630
+ /**
631
+ * Get data for journey by id/name
632
+ * @param {String} journeyId journey id/name
633
+ * @returns {Object} object containing all journey data
634
+ */
635
+ export async function getJourneyData(journeyId) {
636
+ showSpinner(`${journeyId}`);
637
+ const journeyData = getSingleTreeFileDataTemplate();
638
+ const treeData = (
639
+ await getTree(journeyId).catch((err) => {
640
+ succeedSpinner();
641
+ printMessage(err, 'error');
642
+ })
643
+ ).data;
644
+ spinSpinner();
645
+ await exportTree(treeData, journeyData, { useStringArrays: true });
646
+ succeedSpinner();
647
+ return journeyData;
648
+ }
649
+
650
+ /**
651
+ * Helper to import a tree with all dependencies from an import data object (typically read from a file)
652
+ * @param {Object} treeObject tree object containing tree and all its dependencies
653
+ * @param {Object} options reUuid:boolean: re-uuid all node objects, verbose:boolean: verbose output
654
+ */
655
+ async function importTree(treeObject, options) {
656
+ const { reUuid } = options;
657
+ const { deps } = options;
658
+ const { verbose } = options;
659
+ if (verbose) printMessage(`\n- ${treeObject.tree._id}\n`, 'info', false);
660
+ let newUuid = '';
661
+ const uuidMap = {};
662
+ const treeId = treeObject.tree._id;
663
+
664
+ // Process scripts
665
+ if (
666
+ deps &&
667
+ treeObject.scripts &&
668
+ Object.entries(treeObject.scripts).length > 0
669
+ ) {
670
+ if (verbose) printMessage(' - Scripts:');
671
+ for (const [scriptId, scriptObject] of Object.entries(treeObject.scripts)) {
672
+ if (verbose)
673
+ printMessage(` - ${scriptId} (${scriptObject.name})`, 'info', false);
674
+ // is the script stored as an array of strings or just b64 blob?
675
+ if (Array.isArray(scriptObject.script)) {
676
+ scriptObject.script = convertTextArrayToBase64(scriptObject.script);
677
+ } else if (!isBase64Encoded(scriptObject.script)) {
678
+ scriptObject.script = encode(JSON.parse(scriptObject.script));
679
+ }
680
+ // eslint-disable-next-line no-await-in-loop
681
+ if ((await createOrUpdateScript(scriptId, scriptObject)) == null) {
682
+ throw new Error(
683
+ `Error importing script ${scriptObject.name} (${scriptId}) in journey ${treeId}`
684
+ );
685
+ }
686
+ if (verbose) printMessage('');
687
+ }
688
+ }
689
+
690
+ // Process email templates
691
+ if (
692
+ deps &&
693
+ treeObject.emailTemplates &&
694
+ Object.entries(treeObject.emailTemplates).length > 0
695
+ ) {
696
+ if (verbose) printMessage(' - Email templates:');
697
+ for (const [templateId, templateData] of Object.entries(
698
+ treeObject.emailTemplates
699
+ )) {
700
+ if (verbose) printMessage(` - ${templateId}`, 'info', false);
701
+ try {
702
+ // eslint-disable-next-line no-await-in-loop
703
+ await putEmailTemplate(templateId, templateData);
704
+ } catch (error) {
705
+ printMessage(error.response.data, 'error');
706
+ throw new Error(`Error importing email templates: ${error.message}`);
707
+ }
708
+ if (verbose) printMessage('');
709
+ }
710
+ }
711
+
712
+ // Process themes
713
+ if (deps && treeObject.themes && treeObject.themes.length > 0) {
714
+ if (verbose) printMessage(' - Themes:');
715
+ const themes = {};
716
+ for (const theme of treeObject.themes) {
717
+ if (verbose) printMessage(` - ${theme._id} (${theme.name})`, 'info');
718
+ themes[theme._id] = theme;
719
+ }
720
+ try {
721
+ await putThemes(themes);
722
+ } catch (error) {
723
+ throw new Error(`Error importing themes: ${error.message}`);
724
+ }
725
+ }
726
+
727
+ // Process social providers
728
+ if (
729
+ deps &&
730
+ treeObject.socialIdentityProviders &&
731
+ Object.entries(treeObject.socialIdentityProviders).length > 0
732
+ ) {
733
+ if (verbose) printMessage(' - OAuth2/OIDC (social) identity providers:');
734
+ for (const [providerId, providerData] of Object.entries(
735
+ treeObject.socialIdentityProviders
736
+ )) {
737
+ if (verbose) printMessage(` - ${providerId}`, 'info');
738
+ try {
739
+ // eslint-disable-next-line no-await-in-loop
740
+ await putProviderByTypeAndId(
741
+ providerData._type._id,
742
+ providerId,
743
+ providerData
744
+ );
745
+ } catch (importError) {
746
+ if (
747
+ importError.response.status === 500 &&
748
+ importError.response.data.message ===
749
+ 'Unable to update SMS config: Data validation failed for the attribute, Redirect after form post URL'
750
+ ) {
751
+ providerData.redirectAfterFormPostURI = '';
752
+ try {
753
+ // eslint-disable-next-line no-await-in-loop
754
+ await putProviderByTypeAndId(
755
+ providerData._type._id,
756
+ providerId,
757
+ providerData
758
+ );
759
+ } catch (importError2) {
760
+ printMessage(importError.response.data, 'error');
761
+ throw new Error(
762
+ `Error importing provider ${providerId} in journey ${treeId}: ${importError}`
763
+ );
764
+ }
765
+ } else {
766
+ printMessage(importError.response.data, 'error');
767
+ throw new Error(
768
+ `\nError importing provider ${providerId} in journey ${treeId}: ${importError}`
769
+ );
770
+ }
771
+ }
772
+ }
773
+ }
774
+
775
+ // Process saml providers
776
+ if (
777
+ deps &&
778
+ treeObject.saml2Entities &&
779
+ Object.entries(treeObject.saml2Entities).length > 0
780
+ ) {
781
+ if (verbose) printMessage(' - SAML2 entity providers:');
782
+ for (const [, providerData] of Object.entries(treeObject.saml2Entities)) {
783
+ delete providerData._rev;
784
+ const { entityId } = providerData;
785
+ const { entityLocation } = providerData;
786
+ if (verbose) printMessage(` - ${entityLocation} ${entityId}`, 'info');
787
+ let metaData = null;
788
+ if (entityLocation === 'remote') {
789
+ if (Array.isArray(providerData.base64EntityXML)) {
790
+ metaData = convertTextArrayToBase64Url(providerData.base64EntityXML);
791
+ } else {
792
+ metaData = providerData.base64EntityXML;
793
+ }
794
+ }
795
+ delete providerData.entityLocation;
796
+ delete providerData.base64EntityXML;
797
+ // create the provider if it doesn't already exist, or just update it
798
+ if (
799
+ // eslint-disable-next-line no-await-in-loop
800
+ (await findProviders(`entityId eq '${entityId}'`, 'location')).data
801
+ .resultCount === 0
802
+ ) {
803
+ // eslint-disable-next-line no-await-in-loop
804
+ await createProvider(entityLocation, providerData, metaData).catch(
805
+ (createProviderErr) => {
806
+ printMessage(createProviderErr.response.data, 'error');
807
+ throw new Error(`Error creating provider ${entityId}`);
808
+ }
809
+ );
810
+ } else {
811
+ // eslint-disable-next-line no-await-in-loop
812
+ await updateProvider(entityLocation, providerData).catch(
813
+ (updateProviderErr) => {
814
+ printMessage(updateProviderErr.response.data, 'error');
815
+ throw new Error(`Error updating provider ${entityId}`);
816
+ }
817
+ );
818
+ }
819
+ }
820
+ }
821
+
822
+ // Process circles of trust
823
+ if (
824
+ deps &&
825
+ treeObject.circlesOfTrust &&
826
+ Object.entries(treeObject.circlesOfTrust).length > 0
827
+ ) {
828
+ if (verbose) printMessage(' - SAML2 circles of trust:');
829
+ for (const [cotId, cotData] of Object.entries(treeObject.circlesOfTrust)) {
830
+ delete cotData._rev;
831
+ if (verbose) printMessage(` - ${cotId}`, 'info');
832
+ // eslint-disable-next-line no-await-in-loop
833
+ await createCircleOfTrust(cotData)
834
+ // eslint-disable-next-line no-unused-vars
835
+ .catch(async (createCotErr) => {
836
+ if (
837
+ createCotErr.response.status === 409 ||
838
+ createCotErr.response.status === 500
839
+ ) {
840
+ await updateCircleOfTrust(cotId, cotData).catch(
841
+ async (updateCotErr) => {
842
+ printMessage(createCotErr.response.data, 'error');
843
+ printMessage(updateCotErr.response.data, 'error');
844
+ throw new Error(
845
+ `Error creating/updating circle of trust ${cotId}`
846
+ );
847
+ }
848
+ );
849
+ } else {
850
+ printMessage(createCotErr.response.data, 'error');
851
+ throw new Error(`Error creating circle of trust ${cotId}`);
852
+ }
853
+ });
854
+ }
855
+ }
856
+
857
+ // Process inner nodes
858
+ let innerNodes = {};
859
+ if (
860
+ treeObject.innerNodes &&
861
+ Object.entries(treeObject.innerNodes).length > 0
862
+ ) {
863
+ innerNodes = treeObject.innerNodes;
864
+ }
865
+ // old export file format
866
+ else if (
867
+ treeObject.innernodes &&
868
+ Object.entries(treeObject.innernodes).length > 0
869
+ ) {
870
+ innerNodes = treeObject.innernodes;
871
+ }
872
+ if (Object.entries(innerNodes).length > 0) {
873
+ if (verbose) printMessage(' - Inner nodes:', 'text', true);
874
+ for (const [innerNodeId, innerNodeData] of Object.entries(innerNodes)) {
875
+ delete innerNodeData._rev;
876
+ const nodeType = innerNodeData._type._id;
877
+ if (!reUuid) {
878
+ newUuid = innerNodeId;
879
+ } else {
880
+ newUuid = uuidv4();
881
+ uuidMap[innerNodeId] = newUuid;
882
+ }
883
+ innerNodeData._id = newUuid;
884
+
885
+ if (verbose)
886
+ printMessage(
887
+ ` - ${newUuid}${reUuid ? '*' : ''} (${nodeType})`,
888
+ 'info',
889
+ false
890
+ );
891
+
892
+ // If the node has an identityResource config setting
893
+ // and the identityResource ends in 'user'
894
+ // and the node's identityResource is the same as the tree's identityResource
895
+ // change it to the current realm managed user identityResource otherwise leave it alone.
896
+ if (
897
+ innerNodeData.identityResource &&
898
+ innerNodeData.identityResource.endsWith('user') &&
899
+ innerNodeData.identityResource === treeObject.tree.identityResource
900
+ ) {
901
+ innerNodeData.identityResource = `managed/${getRealmManagedUser()}`;
902
+ if (verbose)
903
+ printMessage(
904
+ `\n - identityResource: ${innerNodeData.identityResource}`,
905
+ 'info',
906
+ false
907
+ );
908
+ }
909
+ try {
910
+ // eslint-disable-next-line no-await-in-loop
911
+ await putNode(newUuid, nodeType, innerNodeData);
912
+ } catch (nodeImportError) {
913
+ if (
914
+ nodeImportError.response.status === 400 &&
915
+ nodeImportError.response.data.message ===
916
+ 'Data validation failed for the attribute, Script'
917
+ ) {
918
+ throw new Error(
919
+ `Missing script ${
920
+ innerNodeData.script
921
+ } referenced by inner node ${innerNodeId}${
922
+ innerNodeId === newUuid ? '' : ` [${newUuid}]`
923
+ } (${innerNodeData._type._id}) in journey ${treeId}.`
924
+ );
925
+ } else {
926
+ printMessage(nodeImportError.response.data, 'error');
927
+ throw new Error(
928
+ `Error importing inner node ${innerNodeId}${
929
+ innerNodeId === newUuid ? '' : ` [${newUuid}]`
930
+ } in journey ${treeId}`
931
+ );
932
+ }
933
+ }
934
+ if (verbose) printMessage('');
935
+ }
936
+ }
937
+
938
+ // Process nodes
939
+ if (treeObject.nodes && Object.entries(treeObject.nodes).length > 0) {
940
+ if (verbose) printMessage(' - Nodes:');
941
+ // eslint-disable-next-line prefer-const
942
+ for (let [nodeId, nodeData] of Object.entries(treeObject.nodes)) {
943
+ delete nodeData._rev;
944
+ const nodeType = nodeData._type._id;
945
+ if (!reUuid) {
946
+ newUuid = nodeId;
947
+ } else {
948
+ newUuid = uuidv4();
949
+ uuidMap[nodeId] = newUuid;
950
+ }
951
+ nodeData._id = newUuid;
952
+
953
+ if (nodeType === 'PageNode' && reUuid) {
954
+ for (const [, inPageNodeData] of Object.entries(nodeData.nodes)) {
955
+ const currentId = inPageNodeData._id;
956
+ nodeData = JSON.parse(
957
+ replaceAll(JSON.stringify(nodeData), currentId, uuidMap[currentId])
958
+ );
959
+ }
960
+ }
961
+
962
+ if (verbose)
963
+ printMessage(
964
+ ` - ${newUuid}${reUuid ? '*' : ''} (${nodeType})`,
965
+ 'info',
966
+ false
967
+ );
968
+
969
+ // If the node has an identityResource config setting
970
+ // and the identityResource ends in 'user'
971
+ // and the node's identityResource is the same as the tree's identityResource
972
+ // change it to the current realm managed user identityResource otherwise leave it alone.
973
+ if (
974
+ nodeData.identityResource &&
975
+ nodeData.identityResource.endsWith('user') &&
976
+ nodeData.identityResource === treeObject.tree.identityResource
977
+ ) {
978
+ nodeData.identityResource = `managed/${getRealmManagedUser()}`;
979
+ if (verbose)
980
+ printMessage(
981
+ `\n - identityResource: ${nodeData.identityResource}`,
982
+ 'info',
983
+ false
984
+ );
985
+ }
986
+ try {
987
+ // eslint-disable-next-line no-await-in-loop
988
+ await putNode(newUuid, nodeType, nodeData);
989
+ } catch (nodeImportError) {
990
+ if (
991
+ nodeImportError.response.status === 400 &&
992
+ nodeImportError.response.data.message ===
993
+ 'Data validation failed for the attribute, Script'
994
+ ) {
995
+ throw new Error(
996
+ `Missing script ${nodeData.script} referenced by node ${nodeId}${
997
+ nodeId === newUuid ? '' : ` [${newUuid}]`
998
+ } (${nodeData._type._id}) in journey ${treeId}.`
999
+ );
1000
+ } else {
1001
+ printMessage(nodeImportError.response.data, 'error');
1002
+ throw new Error(
1003
+ `Error importing node ${nodeId}${
1004
+ nodeId === newUuid ? '' : ` [${newUuid}]`
1005
+ } in journey ${treeId}`
1006
+ );
1007
+ }
1008
+ }
1009
+ if (verbose) printMessage('');
1010
+ }
1011
+ }
1012
+
1013
+ // Process tree
1014
+ if (verbose) printMessage(' - Flow');
1015
+
1016
+ if (reUuid) {
1017
+ let journeyText = JSON.stringify(treeObject.tree, null, 2);
1018
+ for (const [oldId, newId] of Object.entries(uuidMap)) {
1019
+ journeyText = replaceAll(journeyText, oldId, newId);
1020
+ }
1021
+ treeObject.tree = JSON.parse(journeyText);
1022
+ }
1023
+
1024
+ // If the tree has an identityResource config setting
1025
+ // and the identityResource ends in 'user'
1026
+ // Set the identityResource for the tree to the selected resource.
1027
+ if (
1028
+ treeObject.tree.identityResource &&
1029
+ treeObject.tree.identityResource.endsWith('user')
1030
+ ) {
1031
+ treeObject.tree.identityResource = `managed/${getRealmManagedUser()}`;
1032
+ if (verbose)
1033
+ printMessage(
1034
+ ` - identityResource: ${treeObject.tree.identityResource}`,
1035
+ 'info',
1036
+ false
1037
+ );
1038
+ }
1039
+
1040
+ delete treeObject.tree._rev;
1041
+ try {
1042
+ await putTree(treeObject.tree._id, treeObject.tree);
1043
+ if (verbose) printMessage(`\n - Done`, 'info', true);
1044
+ } catch (importError) {
1045
+ if (
1046
+ importError.response.status === 400 &&
1047
+ importError.response.data.message === 'Invalid attribute specified.'
1048
+ ) {
1049
+ const { validAttributes } = importError.response.data.detail;
1050
+ validAttributes.push('_id');
1051
+ Object.keys(treeObject.tree).forEach((attribute) => {
1052
+ if (!validAttributes.includes(attribute)) {
1053
+ if (verbose)
1054
+ printMessage(
1055
+ `\n - Removing invalid attribute: ${attribute}`,
1056
+ 'info',
1057
+ false
1058
+ );
1059
+ delete treeObject.tree[attribute];
1060
+ }
1061
+ });
1062
+ try {
1063
+ await putTree(treeObject.tree._id, treeObject.tree);
1064
+ if (verbose) printMessage(`\n - Done`, 'info', true);
1065
+ } catch (importError2) {
1066
+ printMessage(importError2.response.data, 'error');
1067
+ throw new Error(`Error importing journey flow ${treeId}`);
1068
+ }
1069
+ } else {
1070
+ printMessage(importError.response.data, 'error');
1071
+ throw new Error(`\nError importing journey flow ${treeId}`);
1072
+ }
1073
+ }
1074
+ }
1075
+
1076
+ /**
1077
+ * Resolve journey dependencies
1078
+ * @param {Map} installedJorneys Map of installed journeys
1079
+ * @param {Map} journeyMap Map of journeys to resolve dependencies for
1080
+ * @param {[String]} unresolvedJourneys Map to hold the names of unresolved journeys and their dependencies
1081
+ * @param {[String]} resolvedJourneys Array to hold the names of resolved journeys
1082
+ * @param {int} index Depth of recursion
1083
+ */
1084
+ async function resolveDependencies(
1085
+ installedJorneys,
1086
+ journeyMap,
1087
+ unresolvedJourneys,
1088
+ resolvedJourneys,
1089
+ index = -1
1090
+ ) {
1091
+ let before = -1;
1092
+ let after = index;
1093
+ if (index !== -1) {
1094
+ before = index;
1095
+ }
1096
+
1097
+ for (const tree in journeyMap) {
1098
+ if ({}.hasOwnProperty.call(journeyMap, tree)) {
1099
+ const dependencies = [];
1100
+ for (const node in journeyMap[tree].nodes) {
1101
+ if (
1102
+ journeyMap[tree].nodes[node]._type._id === 'InnerTreeEvaluatorNode'
1103
+ ) {
1104
+ dependencies.push(journeyMap[tree].nodes[node].tree);
1105
+ }
1106
+ }
1107
+ let allResolved = true;
1108
+ for (const dependency of dependencies) {
1109
+ if (
1110
+ !resolvedJourneys.includes(dependency) &&
1111
+ !installedJorneys.includes(dependency)
1112
+ ) {
1113
+ allResolved = false;
1114
+ }
1115
+ }
1116
+ if (allResolved) {
1117
+ if (resolvedJourneys.indexOf(tree) === -1) resolvedJourneys.push(tree);
1118
+ // remove from unresolvedJourneys array
1119
+ // for (let i = 0; i < unresolvedJourneys.length; i += 1) {
1120
+ // if (unresolvedJourneys[i] === tree) {
1121
+ // unresolvedJourneys.splice(i, 1);
1122
+ // i -= 1;
1123
+ // }
1124
+ // }
1125
+ delete unresolvedJourneys[tree];
1126
+ // } else if (!unresolvedJourneys.includes(tree)) {
1127
+ } else {
1128
+ // unresolvedJourneys.push(tree);
1129
+ unresolvedJourneys[tree] = dependencies;
1130
+ }
1131
+ }
1132
+ }
1133
+ after = Object.keys(unresolvedJourneys).length;
1134
+ if (index !== -1 && after === before) {
1135
+ // This is the end, no progress was made since the last recursion
1136
+ // printMessage(
1137
+ // `Journeys with unresolved dependencies: ${unresolvedJourneys}`,
1138
+ // 'error'
1139
+ // );
1140
+ } else if (after > 0) {
1141
+ resolveDependencies(
1142
+ installedJorneys,
1143
+ journeyMap,
1144
+ unresolvedJourneys,
1145
+ resolvedJourneys,
1146
+ after
1147
+ );
1148
+ }
1149
+ }
1150
+
1151
+ /**
1152
+ * Import a journey from file
1153
+ * @param {String} journeyId journey id/name
1154
+ * @param {String} file import file name
1155
+ * @param {boolean} options reUuid:boolean: re-uuid all node objects, verbose:boolean: verbose output
1156
+ */
1157
+ export async function importJourneyFromFile(journeyId, file, options) {
1158
+ const { verbose } = options;
1159
+ fs.readFile(file, 'utf8', async (err, data) => {
1160
+ if (err) throw err;
1161
+ let journeyData = JSON.parse(data);
1162
+ // check if this is a file with multiple trees and get journey by id
1163
+ if (journeyData.trees && journeyData.trees[journeyId]) {
1164
+ journeyData = journeyData.trees[journeyId];
1165
+ } else if (journeyData.trees) {
1166
+ journeyData = null;
1167
+ }
1168
+
1169
+ // if a journeyId was specified, only import the matching journey
1170
+ if (journeyData && journeyId === journeyData.tree._id) {
1171
+ // attempt dependency resolution for single tree import
1172
+ const installedJourneys = (await getTrees()).data.result.map(
1173
+ (x) => x._id
1174
+ );
1175
+ const unresolvedJourneys = {};
1176
+ const resolvedJourneys = [];
1177
+ showSpinner('Resolving dependencies');
1178
+ await resolveDependencies(
1179
+ installedJourneys,
1180
+ { [journeyId]: journeyData },
1181
+ unresolvedJourneys,
1182
+ resolvedJourneys
1183
+ );
1184
+ if (Object.keys(unresolvedJourneys).length === 0) {
1185
+ succeedSpinner(`Resolved all dependencies.`);
1186
+
1187
+ if (!verbose) showSpinner(`Importing ${journeyId}...`);
1188
+ importTree(journeyData, options)
1189
+ .then(() => {
1190
+ if (verbose) showSpinner(`Importing ${journeyId}...`);
1191
+ succeedSpinner(`Imported ${journeyId}.`);
1192
+ })
1193
+ .catch((importError) => {
1194
+ if (verbose) showSpinner(`Importing ${journeyId}...`);
1195
+ failSpinner(`${importError}`);
1196
+ });
1197
+ } else {
1198
+ failSpinner(`Unresolved dependencies:`);
1199
+ for (const journey of Object.keys(unresolvedJourneys)) {
1200
+ printMessage(
1201
+ ` ${journey} requires ${unresolvedJourneys[journey]}`,
1202
+ 'error'
1203
+ );
1204
+ }
1205
+ }
1206
+ // end dependency resolution for single tree import
1207
+ } else {
1208
+ showSpinner(`Importing ${journeyId}...`);
1209
+ failSpinner(`${journeyId} not found!`);
1210
+ }
1211
+ });
1212
+ }
1213
+
1214
+ /**
1215
+ * Import first journey from file
1216
+ * @param {String} file import file name
1217
+ * @param {boolean} options reUuid:boolean: re-uuid all node objects, verbose:boolean: verbose output
1218
+ */
1219
+ export async function importFirstJourneyFromFile(file, options) {
1220
+ const { verbose } = options;
1221
+ fs.readFile(file, 'utf8', async (err, data) => {
1222
+ if (err) throw err;
1223
+ let journeyData = _.cloneDeep(JSON.parse(data));
1224
+ let journeyId = null;
1225
+ // single tree
1226
+ if (journeyData.tree) {
1227
+ journeyId = _.cloneDeep(journeyData.tree._id);
1228
+ }
1229
+ // multiple trees, so get the first tree
1230
+ else if (journeyData.trees) {
1231
+ for (const treeId in journeyData.trees) {
1232
+ if (Object.hasOwnProperty.call(journeyData.trees, treeId)) {
1233
+ journeyId = treeId;
1234
+ journeyData = journeyData.trees[treeId];
1235
+ break;
1236
+ }
1237
+ }
1238
+ }
1239
+
1240
+ // if a journeyId was specified, only import the matching journey
1241
+ if (journeyData && journeyId) {
1242
+ // attempt dependency resolution for single tree import
1243
+ const installedJourneys = (await getTrees()).data.result.map(
1244
+ (x) => x._id
1245
+ );
1246
+ const unresolvedJourneys = {};
1247
+ const resolvedJourneys = [];
1248
+ showSpinner('Resolving dependencies');
1249
+ await resolveDependencies(
1250
+ installedJourneys,
1251
+ { [journeyId]: journeyData },
1252
+ unresolvedJourneys,
1253
+ resolvedJourneys
1254
+ );
1255
+ if (Object.keys(unresolvedJourneys).length === 0) {
1256
+ succeedSpinner(`Resolved all dependencies.`);
1257
+
1258
+ if (!verbose) showSpinner(`Importing ${journeyId}...`);
1259
+ importTree(journeyData, options)
1260
+ .then(() => {
1261
+ if (verbose) showSpinner(`Importing ${journeyId}...`);
1262
+ succeedSpinner(`Imported ${journeyId}.`);
1263
+ })
1264
+ .catch((importError) => {
1265
+ if (verbose) showSpinner(`Importing ${journeyId}...`);
1266
+ failSpinner(`${importError}`);
1267
+ });
1268
+ } else {
1269
+ failSpinner(`Unresolved dependencies:`);
1270
+ for (const journey of Object.keys(unresolvedJourneys)) {
1271
+ printMessage(
1272
+ ` ${journey} requires ${unresolvedJourneys[journey]}`,
1273
+ 'error'
1274
+ );
1275
+ }
1276
+ }
1277
+ } else {
1278
+ showSpinner(`Importing...`);
1279
+ failSpinner(`No journeys found!`);
1280
+ }
1281
+ // end dependency resolution for single tree import
1282
+ });
1283
+ }
1284
+
1285
+ /**
1286
+ * Helper to import multiple trees from a tree map
1287
+ * @param {Object} treesMap map of trees object
1288
+ * @param {boolean} options reUuid:boolean: re-uuid all node objects, verbose:boolean: verbose output
1289
+ */
1290
+ async function importAllTrees(treesMap, options) {
1291
+ const installedJourneys = (await getTrees()).data.result.map((x) => x._id);
1292
+ const unresolvedJourneys = {};
1293
+ const resolvedJourneys = [];
1294
+ showSpinner('Resolving dependencies');
1295
+ await resolveDependencies(
1296
+ installedJourneys,
1297
+ treesMap,
1298
+ unresolvedJourneys,
1299
+ resolvedJourneys
1300
+ );
1301
+ if (Object.keys(unresolvedJourneys).length === 0) {
1302
+ succeedSpinner(`Resolved all dependencies.`);
1303
+ } else {
1304
+ failSpinner(
1305
+ `${
1306
+ Object.keys(unresolvedJourneys).length
1307
+ } journeys with unresolved dependencies:`
1308
+ );
1309
+ for (const journey of Object.keys(unresolvedJourneys)) {
1310
+ printMessage(
1311
+ ` - ${journey} requires ${unresolvedJourneys[journey]}`,
1312
+ 'info'
1313
+ );
1314
+ }
1315
+ }
1316
+ createProgressBar(resolvedJourneys.length, 'Importing');
1317
+ for (const tree of resolvedJourneys) {
1318
+ try {
1319
+ // eslint-disable-next-line no-await-in-loop
1320
+ await importTree(treesMap[tree], options);
1321
+ updateProgressBar(`${tree}`);
1322
+ } catch (error) {
1323
+ printMessage(`\n${error.message}`, 'error');
1324
+ }
1325
+ }
1326
+ stopProgressBar('Done');
1327
+ }
1328
+
1329
+ /**
1330
+ * Import all journeys from file
1331
+ * @param {*} file import file name
1332
+ * @param {boolean} options reUuid:boolean: re-uuid all node objects, verbose:boolean: verbose output
1333
+ */
1334
+ export async function importJourneysFromFile(file, options) {
1335
+ fs.readFile(file, 'utf8', (err, data) => {
1336
+ if (err) throw err;
1337
+ const fileData = JSON.parse(data);
1338
+ importAllTrees(fileData.trees, options);
1339
+ });
1340
+ }
1341
+
1342
+ /**
1343
+ * Import all journeys from separate files
1344
+ * @param {boolean} options reUuid:boolean: re-uuid all node objects, verbose:boolean: verbose output
1345
+ */
1346
+ export async function importJourneysFromFiles(options) {
1347
+ const names = fs.readdirSync('.');
1348
+ const jsonFiles = names.filter((name) =>
1349
+ name.toLowerCase().endsWith('.journey.json')
1350
+ );
1351
+ const allJourneysData = { trees: {} };
1352
+ for (const file of jsonFiles) {
1353
+ const journeyData = JSON.parse(fs.readFileSync(file, 'utf8'));
1354
+ allJourneysData.trees[journeyData.tree._id] = journeyData;
1355
+ }
1356
+ importAllTrees(allJourneysData.trees, options);
1357
+ }
1358
+
1359
+ /**
1360
+ * Describe a tree
1361
+ * @param {Object} treeData tree
1362
+ * @returns {Object} an object describing the tree
1363
+ */
1364
+ export function describeTree(treeData) {
1365
+ const treeMap = {};
1366
+ const nodeTypeMap = {};
1367
+ const scriptsMap = {};
1368
+ const emailTemplatesMap = {};
1369
+ treeMap.treeName = treeData.tree._id;
1370
+ for (const [, nodeData] of Object.entries(treeData.nodes)) {
1371
+ if (nodeTypeMap[nodeData._type._id]) {
1372
+ nodeTypeMap[nodeData._type._id] += 1;
1373
+ } else {
1374
+ nodeTypeMap[nodeData._type._id] = 1;
1375
+ }
1376
+ }
1377
+
1378
+ for (const [, nodeData] of Object.entries(treeData.innerNodes)) {
1379
+ if (nodeTypeMap[nodeData._type._id]) {
1380
+ nodeTypeMap[nodeData._type._id] += 1;
1381
+ } else {
1382
+ nodeTypeMap[nodeData._type._id] = 1;
1383
+ }
1384
+ }
1385
+
1386
+ for (const [, scriptData] of Object.entries(treeData.scripts)) {
1387
+ scriptsMap[scriptData.name] = scriptData.description;
1388
+ }
1389
+
1390
+ for (const [id, data] of Object.entries(treeData.emailTemplates)) {
1391
+ emailTemplatesMap[id] = data.displayName;
1392
+ }
1393
+
1394
+ treeMap.nodeTypes = nodeTypeMap;
1395
+ treeMap.scripts = scriptsMap;
1396
+ treeMap.emailTemplates = emailTemplatesMap;
1397
+ return treeMap;
1398
+ }
1399
+
1400
+ /**
1401
+ * Find all node configuration objects that are no longer referenced by any tree
1402
+ */
1403
+ async function findOrphanedNodes() {
1404
+ const allNodes = [];
1405
+ const orphanedNodes = [];
1406
+ let types = [];
1407
+ const allJourneys = (await getTrees()).data.result;
1408
+ let errorMessage = '';
1409
+ const errorTypes = [];
1410
+
1411
+ showSpinner(`Counting total nodes...`);
1412
+ try {
1413
+ types = (await getNodeTypes()).data.result;
1414
+ } catch (error) {
1415
+ printMessage('Error retrieving all available node types:', 'error');
1416
+ printMessage(error.response.data, 'error');
1417
+ return [];
1418
+ }
1419
+ for (const type of types) {
1420
+ try {
1421
+ // eslint-disable-next-line no-await-in-loop, no-loop-func
1422
+ (await getNodesByType(type._id)).data.result.forEach((node) => {
1423
+ allNodes.push(node);
1424
+ spinSpinner(`${allNodes.length} total nodes${errorMessage}`);
1425
+ });
1426
+ } catch (error) {
1427
+ errorTypes.push(type._id);
1428
+ errorMessage = ` (Skipped type(s): ${errorTypes})`.yellow;
1429
+ spinSpinner(`${allNodes.length} total nodes${errorMessage}`);
1430
+ }
1431
+ }
1432
+ if (errorTypes.length > 0) {
1433
+ warnSpinner(`${allNodes.length} total nodes${errorMessage}`);
1434
+ } else {
1435
+ succeedSpinner(`${allNodes.length} total nodes`);
1436
+ }
1437
+
1438
+ showSpinner('Counting active nodes...');
1439
+ const activeNodes = [];
1440
+ for (const journey of allJourneys) {
1441
+ for (const nodeId in journey.nodes) {
1442
+ if ({}.hasOwnProperty.call(journey.nodes, nodeId)) {
1443
+ activeNodes.push(nodeId);
1444
+ spinSpinner(`${activeNodes.length} active nodes`);
1445
+ const node = journey.nodes[nodeId];
1446
+ if (containerNodes.includes(node.nodeType)) {
1447
+ // eslint-disable-next-line no-await-in-loop
1448
+ const containerNode = (await getNode(nodeId, node.nodeType)).data;
1449
+ containerNode.nodes.forEach((n) => {
1450
+ activeNodes.push(n._id);
1451
+ spinSpinner(`${activeNodes.length} active nodes`);
1452
+ });
1453
+ }
1454
+ }
1455
+ }
1456
+ }
1457
+ succeedSpinner(`${activeNodes.length} active nodes`);
1458
+
1459
+ showSpinner('Calculating orphaned nodes...');
1460
+ const diff = allNodes.filter((x) => !activeNodes.includes(x._id));
1461
+ diff.forEach((x) => orphanedNodes.push(x));
1462
+ succeedSpinner(`${orphanedNodes.length} orphaned nodes`);
1463
+ return orphanedNodes;
1464
+ }
1465
+
1466
+ /**
1467
+ * Remove orphaned nodes
1468
+ * @param {[Object]} orphanedNodes Pass in an array of orphaned node configuration objects to remove
1469
+ */
1470
+ async function removeOrphanedNodes(orphanedNodes) {
1471
+ createProgressBar(orphanedNodes.length, 'Removing orphaned nodes...');
1472
+ for (const node of orphanedNodes) {
1473
+ updateProgressBar(`Removing ${node._id}...`);
1474
+ // eslint-disable-next-line no-await-in-loop
1475
+ await deleteNode(node._id, node._type._id).catch((deleteError) => {
1476
+ printMessage(`${deleteError}`, 'error');
1477
+ });
1478
+ }
1479
+ stopProgressBar(`Removed ${orphanedNodes.length} orphaned nodes.`);
1480
+ }
1481
+
1482
+ /**
1483
+ * Prune orphaned nodes
1484
+ */
1485
+ export async function prune() {
1486
+ const orphanedNodes = await findOrphanedNodes();
1487
+ if (orphanedNodes.length > 0) {
1488
+ const ok = await yesno({
1489
+ question: 'Prune (permanently delete) orphaned nodes? (y|n):',
1490
+ });
1491
+ if (ok) {
1492
+ await removeOrphanedNodes(orphanedNodes);
1493
+ }
1494
+ } else {
1495
+ printMessage('No orphaned nodes found.');
1496
+ }
1497
+ }
1498
+
1499
+ const OOTB_NODE_TYPES_7 = [
1500
+ 'AcceptTermsAndConditionsNode',
1501
+ 'AccountActiveDecisionNode',
1502
+ 'AccountLockoutNode',
1503
+ 'AgentDataStoreDecisionNode',
1504
+ 'AnonymousSessionUpgradeNode',
1505
+ 'AnonymousUserNode',
1506
+ 'AttributeCollectorNode',
1507
+ 'AttributePresentDecisionNode',
1508
+ 'AttributeValueDecisionNode',
1509
+ 'AuthLevelDecisionNode',
1510
+ 'ChoiceCollectorNode',
1511
+ 'ConsentNode',
1512
+ 'CookiePresenceDecisionNode',
1513
+ 'CreateObjectNode',
1514
+ 'CreatePasswordNode',
1515
+ 'DataStoreDecisionNode',
1516
+ 'DeviceGeoFencingNode',
1517
+ 'DeviceLocationMatchNode',
1518
+ 'DeviceMatchNode',
1519
+ 'DeviceProfileCollectorNode',
1520
+ 'DeviceSaveNode',
1521
+ 'DeviceTamperingVerificationNode',
1522
+ 'DisplayUserNameNode',
1523
+ 'EmailSuspendNode',
1524
+ 'EmailTemplateNode',
1525
+ 'IdentifyExistingUserNode',
1526
+ 'IncrementLoginCountNode',
1527
+ 'InnerTreeEvaluatorNode',
1528
+ 'IotAuthenticationNode',
1529
+ 'IotRegistrationNode',
1530
+ 'KbaCreateNode',
1531
+ 'KbaDecisionNode',
1532
+ 'KbaVerifyNode',
1533
+ 'LdapDecisionNode',
1534
+ 'LoginCountDecisionNode',
1535
+ 'MessageNode',
1536
+ 'MetadataNode',
1537
+ 'MeterNode',
1538
+ 'ModifyAuthLevelNode',
1539
+ 'OneTimePasswordCollectorDecisionNode',
1540
+ 'OneTimePasswordGeneratorNode',
1541
+ 'OneTimePasswordSmsSenderNode',
1542
+ 'OneTimePasswordSmtpSenderNode',
1543
+ 'PageNode',
1544
+ 'PasswordCollectorNode',
1545
+ 'PatchObjectNode',
1546
+ 'PersistentCookieDecisionNode',
1547
+ 'PollingWaitNode',
1548
+ 'ProfileCompletenessDecisionNode',
1549
+ 'ProvisionDynamicAccountNode',
1550
+ 'ProvisionIdmAccountNode',
1551
+ 'PushAuthenticationSenderNode',
1552
+ 'PushResultVerifierNode',
1553
+ 'QueryFilterDecisionNode',
1554
+ 'RecoveryCodeCollectorDecisionNode',
1555
+ 'RecoveryCodeDisplayNode',
1556
+ 'RegisterLogoutWebhookNode',
1557
+ 'RemoveSessionPropertiesNode',
1558
+ 'RequiredAttributesDecisionNode',
1559
+ 'RetryLimitDecisionNode',
1560
+ 'ScriptedDecisionNode',
1561
+ 'SelectIdPNode',
1562
+ 'SessionDataNode',
1563
+ 'SetFailureUrlNode',
1564
+ 'SetPersistentCookieNode',
1565
+ 'SetSessionPropertiesNode',
1566
+ 'SetSuccessUrlNode',
1567
+ 'SocialFacebookNode',
1568
+ 'SocialGoogleNode',
1569
+ 'SocialNode',
1570
+ 'SocialOAuthIgnoreProfileNode',
1571
+ 'SocialOpenIdConnectNode',
1572
+ 'SocialProviderHandlerNode',
1573
+ 'TermsAndConditionsDecisionNode',
1574
+ 'TimeSinceDecisionNode',
1575
+ 'TimerStartNode',
1576
+ 'TimerStopNode',
1577
+ 'UsernameCollectorNode',
1578
+ 'ValidatedPasswordNode',
1579
+ 'ValidatedUsernameNode',
1580
+ 'WebAuthnAuthenticationNode',
1581
+ 'WebAuthnDeviceStorageNode',
1582
+ 'WebAuthnRegistrationNode',
1583
+ 'ZeroPageLoginNode',
1584
+ 'product-CertificateCollectorNode',
1585
+ 'product-CertificateUserExtractorNode',
1586
+ 'product-CertificateValidationNode',
1587
+ 'product-KerberosNode',
1588
+ 'product-ReCaptchaNode',
1589
+ 'product-Saml2Node',
1590
+ 'product-WriteFederationInformationNode',
1591
+ ];
1592
+
1593
+ const OOTB_NODE_TYPES_7_1 = [
1594
+ 'PushRegistrationNode',
1595
+ 'GetAuthenticatorAppNode',
1596
+ 'MultiFactorRegistrationOptionsNode',
1597
+ 'OptOutMultiFactorAuthenticationNode',
1598
+ ].concat(OOTB_NODE_TYPES_7);
1599
+
1600
+ const OOTB_NODE_TYPES_7_2 = [
1601
+ 'OathRegistrationNode',
1602
+ 'OathTokenVerifierNode',
1603
+ 'PassthroughAuthenticationNode',
1604
+ 'ConfigProviderNode',
1605
+ 'DebugNode',
1606
+ ].concat(OOTB_NODE_TYPES_7_1);
1607
+
1608
+ const OOTB_NODE_TYPES_6_5 = [
1609
+ 'AbstractSocialAuthLoginNode',
1610
+ 'AccountLockoutNode',
1611
+ 'AgentDataStoreDecisionNode',
1612
+ 'AnonymousUserNode',
1613
+ 'AuthLevelDecisionNode',
1614
+ 'ChoiceCollectorNode',
1615
+ 'CookiePresenceDecisionNode',
1616
+ 'CreatePasswordNode',
1617
+ 'DataStoreDecisionNode',
1618
+ 'InnerTreeEvaluatorNode',
1619
+ 'LdapDecisionNode',
1620
+ 'MessageNode',
1621
+ 'MetadataNode',
1622
+ 'MeterNode',
1623
+ 'ModifyAuthLevelNode',
1624
+ 'OneTimePasswordCollectorDecisionNode',
1625
+ 'OneTimePasswordGeneratorNode',
1626
+ 'OneTimePasswordSmsSenderNode',
1627
+ 'OneTimePasswordSmtpSenderNode',
1628
+ 'PageNode',
1629
+ 'PasswordCollectorNode',
1630
+ 'PersistentCookieDecisionNode',
1631
+ 'PollingWaitNode',
1632
+ 'ProvisionDynamicAccountNode',
1633
+ 'ProvisionIdmAccountNode',
1634
+ 'PushAuthenticationSenderNode',
1635
+ 'PushResultVerifierNode',
1636
+ 'RecoveryCodeCollectorDecisionNode',
1637
+ 'RecoveryCodeDisplayNode',
1638
+ 'RegisterLogoutWebhookNode',
1639
+ 'RemoveSessionPropertiesNode',
1640
+ 'RetryLimitDecisionNode',
1641
+ 'ScriptedDecisionNode',
1642
+ 'SessionDataNode',
1643
+ 'SetFailureUrlNode',
1644
+ 'SetPersistentCookieNode',
1645
+ 'SetSessionPropertiesNode',
1646
+ 'SetSuccessUrlNode',
1647
+ 'SocialFacebookNode',
1648
+ 'SocialGoogleNode',
1649
+ 'SocialNode',
1650
+ 'SocialOAuthIgnoreProfileNode',
1651
+ 'SocialOpenIdConnectNode',
1652
+ 'TimerStartNode',
1653
+ 'TimerStopNode',
1654
+ 'UsernameCollectorNode',
1655
+ 'WebAuthnAuthenticationNode',
1656
+ 'WebAuthnRegistrationNode',
1657
+ 'ZeroPageLoginNode',
1658
+ ];
1659
+
1660
+ const OOTB_NODE_TYPES_6 = [
1661
+ 'AbstractSocialAuthLoginNode',
1662
+ 'AccountLockoutNode',
1663
+ 'AgentDataStoreDecisionNode',
1664
+ 'AnonymousUserNode',
1665
+ 'AuthLevelDecisionNode',
1666
+ 'ChoiceCollectorNode',
1667
+ 'CookiePresenceDecisionNode',
1668
+ 'CreatePasswordNode',
1669
+ 'DataStoreDecisionNode',
1670
+ 'InnerTreeEvaluatorNode',
1671
+ 'LdapDecisionNode',
1672
+ 'MessageNode',
1673
+ 'MetadataNode',
1674
+ 'MeterNode',
1675
+ 'ModifyAuthLevelNode',
1676
+ 'OneTimePasswordCollectorDecisionNode',
1677
+ 'OneTimePasswordGeneratorNode',
1678
+ 'OneTimePasswordSmsSenderNode',
1679
+ 'OneTimePasswordSmtpSenderNode',
1680
+ 'PageNode',
1681
+ 'PasswordCollectorNode',
1682
+ 'PersistentCookieDecisionNode',
1683
+ 'PollingWaitNode',
1684
+ 'ProvisionDynamicAccountNode',
1685
+ 'ProvisionIdmAccountNode',
1686
+ 'PushAuthenticationSenderNode',
1687
+ 'PushResultVerifierNode',
1688
+ 'RecoveryCodeCollectorDecisionNode',
1689
+ 'RecoveryCodeDisplayNode',
1690
+ 'RegisterLogoutWebhookNode',
1691
+ 'RemoveSessionPropertiesNode',
1692
+ 'RetryLimitDecisionNode',
1693
+ 'ScriptedDecisionNode',
1694
+ 'SessionDataNode',
1695
+ 'SetFailureUrlNode',
1696
+ 'SetPersistentCookieNode',
1697
+ 'SetSessionPropertiesNode',
1698
+ 'SetSuccessUrlNode',
1699
+ 'SocialFacebookNode',
1700
+ 'SocialGoogleNode',
1701
+ 'SocialNode',
1702
+ 'SocialOAuthIgnoreProfileNode',
1703
+ 'SocialOpenIdConnectNode',
1704
+ 'TimerStartNode',
1705
+ 'TimerStopNode',
1706
+ 'UsernameCollectorNode',
1707
+ 'WebAuthnAuthenticationNode',
1708
+ 'WebAuthnRegistrationNode',
1709
+ 'ZeroPageLoginNode',
1710
+ ];
1711
+
1712
+ /**
1713
+ * Analyze if a journey contains any custom nodes considering the detected or the overridden version.
1714
+ * @param {Object} journey Journey/tree configuration object
1715
+ * @returns {boolean} True if the journey/tree contains any custom nodes, false otherwise.
1716
+ */
1717
+ async function isCustom(journey) {
1718
+ let ootbNodeTypes = [];
1719
+ const nodeList = journey.nodes;
1720
+ // console.log(nodeList);
1721
+ // console.log(storage.session.getAmVersion());
1722
+ switch (storage.session.getAmVersion()) {
1723
+ case '7.1.0':
1724
+ ootbNodeTypes = OOTB_NODE_TYPES_7_1.slice(0);
1725
+ break;
1726
+ case '7.2.0':
1727
+ // console.log("here");
1728
+ ootbNodeTypes = OOTB_NODE_TYPES_7_2.slice(0);
1729
+ break;
1730
+ case '7.0.0':
1731
+ case '7.0.1':
1732
+ case '7.0.2':
1733
+ ootbNodeTypes = OOTB_NODE_TYPES_7.slice(0);
1734
+ break;
1735
+ case '6.5.3':
1736
+ case '6.5.2.3':
1737
+ case '6.5.2.2':
1738
+ case '6.5.2.1':
1739
+ case '6.5.2':
1740
+ case '6.5.1':
1741
+ case '6.5.0.2':
1742
+ case '6.5.0.1':
1743
+ ootbNodeTypes = OOTB_NODE_TYPES_6_5.slice(0);
1744
+ break;
1745
+ case '6.0.0.7':
1746
+ case '6.0.0.6':
1747
+ case '6.0.0.5':
1748
+ case '6.0.0.4':
1749
+ case '6.0.0.3':
1750
+ case '6.0.0.2':
1751
+ case '6.0.0.1':
1752
+ case '6.0.0':
1753
+ ootbNodeTypes = OOTB_NODE_TYPES_6.slice(0);
1754
+ break;
1755
+ default:
1756
+ return true;
1757
+ }
1758
+ const results = [];
1759
+ for (const node in nodeList) {
1760
+ if ({}.hasOwnProperty.call(nodeList, node)) {
1761
+ if (!ootbNodeTypes.includes(nodeList[node].nodeType)) {
1762
+ return true;
1763
+ }
1764
+ if (containerNodes.includes(nodeList[node].nodeType)) {
1765
+ results.push(
1766
+ // eslint-disable-next-line no-await-in-loop
1767
+ (await getNode(node, nodeList[node].nodeType)).then(
1768
+ (response) => response.data
1769
+ )
1770
+ );
1771
+ }
1772
+ }
1773
+ }
1774
+ const pageNodes = await Promise.all(results);
1775
+ let custom = false;
1776
+ pageNodes.forEach((pageNode) => {
1777
+ if (pageNode != null) {
1778
+ for (const pnode of pageNode.nodes) {
1779
+ if (!ootbNodeTypes.includes(pnode.nodeType)) {
1780
+ custom = true;
1781
+ }
1782
+ }
1783
+ } else {
1784
+ printMessage(
1785
+ `isCustom ERROR: can't get ${nodeList[pageNode].nodeType} with id ${pageNode} in ${journey._id}`,
1786
+ 'error'
1787
+ );
1788
+ custom = false;
1789
+ }
1790
+ });
1791
+ return custom;
1792
+ }
1793
+
1794
+ /**
1795
+ * List all the journeys/trees
1796
+ * @param {boolean} long Long version, all the fields
1797
+ * @param {boolean} analyze Analyze journeys/trees for custom nodes (expensive)
1798
+ */
1799
+ export async function listJourneys(long = false, analyze = false) {
1800
+ let journeys = [];
1801
+ try {
1802
+ journeys = (await getTrees()).data.result;
1803
+ } catch (error) {
1804
+ printMessage(`${error.message}`, 'error');
1805
+ printMessage(error.response.data, 'error');
1806
+ }
1807
+ journeys.sort((a, b) => a._id.localeCompare(b._id));
1808
+ let customTrees = Array(journeys.length).fill(false);
1809
+ if (analyze) {
1810
+ const results = [];
1811
+ for (const journey of journeys) {
1812
+ results.push(isCustom(journey));
1813
+ }
1814
+ customTrees = await Promise.all(results);
1815
+ }
1816
+ if (!long) {
1817
+ for (const [i, journey] of journeys.entries()) {
1818
+ printMessage(`${customTrees[i] ? '*' : ''}${journey._id}`, 'data');
1819
+ }
1820
+ } else {
1821
+ const table = createTable([
1822
+ 'Name'.brightCyan,
1823
+ 'Status'.brightCyan,
1824
+ 'Tags'.brightCyan,
1825
+ ]);
1826
+ journeys.forEach((journey, i) => {
1827
+ table.push([
1828
+ `${customTrees[i] ? '*'.brightRed : ''}${journey._id}`,
1829
+ journey.enabled === false
1830
+ ? 'disabled'.brightRed
1831
+ : 'enabled'.brightGreen,
1832
+ journey.uiConfig && journey.uiConfig.categories
1833
+ ? wordwrap(JSON.parse(journey.uiConfig.categories).join(', '), 60)
1834
+ : '',
1835
+ ]);
1836
+ });
1837
+ printMessage(table.toString(), 'data');
1838
+ }
1839
+ }
1840
+
1841
+ /**
1842
+ * Delete a journey
1843
+ * @param {String} journeyId journey id/name
1844
+ * @param {Object} options deep=true also delete all the nodes and inner nodes, verbose=true print verbose info
1845
+ */
1846
+ export async function deleteJourney(journeyId, options, spinner = true) {
1847
+ const { deep } = options;
1848
+ const { verbose } = options;
1849
+ const status = { nodes: {} };
1850
+ if (spinner) showSpinner(`Deleting ${journeyId}...`);
1851
+ if (spinner && verbose) stopSpinner();
1852
+ return deleteTree(journeyId)
1853
+ .then(async (deleteTreeResponse) => {
1854
+ status.status = 'success';
1855
+ const nodePromises = [];
1856
+ if (verbose) printMessage(`Deleted ${journeyId} (tree)`, 'info');
1857
+ if (deep) {
1858
+ for (const [nodeId, nodeObject] of Object.entries(
1859
+ deleteTreeResponse.data.nodes
1860
+ )) {
1861
+ // delete inner nodes (nodes inside container nodes)
1862
+ if (containerNodes.includes(nodeObject.nodeType)) {
1863
+ try {
1864
+ // eslint-disable-next-line no-await-in-loop
1865
+ const pageNode = (await getNode(nodeId, nodeObject.nodeType))
1866
+ .data;
1867
+ if (verbose)
1868
+ printMessage(
1869
+ `Read ${nodeId} (${nodeObject.nodeType}) from ${journeyId}`,
1870
+ 'info'
1871
+ );
1872
+ for (const innerNodeObject of pageNode.nodes) {
1873
+ nodePromises.push(
1874
+ deleteNode(innerNodeObject._id, innerNodeObject.nodeType)
1875
+ .then((response2) => {
1876
+ status.nodes[innerNodeObject._id] = { status: 'success' };
1877
+ if (verbose)
1878
+ printMessage(
1879
+ `Deleted ${innerNodeObject._id} (${innerNodeObject.nodeType}) from ${journeyId}`,
1880
+ 'info'
1881
+ );
1882
+ return response2.data;
1883
+ })
1884
+ .catch((error) => {
1885
+ status.nodes[innerNodeObject._id] = {
1886
+ status: 'error',
1887
+ error,
1888
+ };
1889
+ if (verbose)
1890
+ printMessage(
1891
+ `Error deleting inner node ${innerNodeObject._id} (${innerNodeObject.nodeType}) from ${journeyId}: ${error}`,
1892
+ 'error'
1893
+ );
1894
+ })
1895
+ );
1896
+ }
1897
+ // finally delete the container node
1898
+ nodePromises.push(
1899
+ deleteNode(nodeId, nodeObject.nodeType)
1900
+ .then((response2) => {
1901
+ status.nodes[nodeId] = { status: 'success' };
1902
+ if (verbose)
1903
+ printMessage(
1904
+ `Deleted ${nodeId} (${nodeObject.nodeType}) from ${journeyId}`,
1905
+ 'info'
1906
+ );
1907
+ return response2.data;
1908
+ })
1909
+ .catch((error) => {
1910
+ status.nodes[nodeId] = { status: 'error', error };
1911
+ if (verbose)
1912
+ printMessage(
1913
+ `Error deleting container node ${nodeId} (${nodeObject.nodeType}) from ${journeyId}: ${error}`,
1914
+ 'error'
1915
+ );
1916
+ })
1917
+ );
1918
+ } catch (error) {
1919
+ if (verbose)
1920
+ printMessage(
1921
+ `Error getting container node ${nodeId} (${nodeObject.nodeType}) from ${journeyId}: ${error}`,
1922
+ 'error'
1923
+ );
1924
+ }
1925
+ } else {
1926
+ // delete the node
1927
+ nodePromises.push(
1928
+ deleteNode(nodeId, nodeObject.nodeType)
1929
+ .then((response) => {
1930
+ status.nodes[nodeId] = { status: 'success' };
1931
+ if (verbose)
1932
+ printMessage(
1933
+ `Deleted ${nodeId} (${nodeObject.nodeType}) from ${journeyId}`,
1934
+ 'info'
1935
+ );
1936
+ return response.data;
1937
+ })
1938
+ .catch((error) => {
1939
+ status.nodes[nodeId] = { status: 'error', error };
1940
+ if (verbose)
1941
+ printMessage(
1942
+ `Error deleting node ${nodeId} (${nodeObject.nodeType}) from ${journeyId}: ${error}`,
1943
+ 'error'
1944
+ );
1945
+ })
1946
+ );
1947
+ }
1948
+ }
1949
+ }
1950
+ // wait until all the node calls are complete
1951
+ await Promise.allSettled(nodePromises);
1952
+
1953
+ // report status
1954
+ if (spinner) {
1955
+ let nodeCount = 0;
1956
+ let errorCount = 0;
1957
+ for (const node of Object.keys(status.nodes)) {
1958
+ nodeCount += 1;
1959
+ if (status.nodes[node].status === 'error') errorCount += 1;
1960
+ }
1961
+ if (errorCount === 0) {
1962
+ succeedSpinner(
1963
+ `Deleted ${journeyId} and ${
1964
+ nodeCount - errorCount
1965
+ }/${nodeCount} nodes.`
1966
+ );
1967
+ } else {
1968
+ failSpinner(
1969
+ `Deleted ${journeyId} and ${
1970
+ nodeCount - errorCount
1971
+ }/${nodeCount} nodes.`
1972
+ );
1973
+ }
1974
+ }
1975
+ return status;
1976
+ })
1977
+ .catch((error) => {
1978
+ status.status = 'error';
1979
+ status.error = error;
1980
+ failSpinner(`Error deleting ${journeyId}.`);
1981
+ if (verbose)
1982
+ printMessage(`Error deleting tree ${journeyId}: ${error}`, 'error');
1983
+ return status;
1984
+ });
1985
+ }
1986
+
1987
+ /**
1988
+ * Delete all journeys
1989
+ * @param {Object} options deep=true also delete all the nodes and inner nodes, verbose=true print verbose info
1990
+ */
1991
+ export async function deleteJourneys(options) {
1992
+ const { verbose } = options;
1993
+ const status = {};
1994
+ const trees = (await getTrees()).data.result;
1995
+ createProgressBar(trees.length, 'Deleting journeys...');
1996
+ for (const tree of trees) {
1997
+ if (verbose) printMessage('');
1998
+ // eslint-disable-next-line no-await-in-loop
1999
+ status[tree._id] = await deleteJourney(tree._id, options, false);
2000
+ updateProgressBar(`${tree._id}`);
2001
+ // introduce a 100ms wait to allow the progress bar to update before the next verbose message prints from the async function
2002
+ if (verbose)
2003
+ // eslint-disable-next-line no-await-in-loop
2004
+ await new Promise((r) => {
2005
+ setTimeout(r, 100);
2006
+ });
2007
+ }
2008
+ let journeyCount = 0;
2009
+ let journeyErrorCount = 0;
2010
+ let nodeCount = 0;
2011
+ let nodeErrorCount = 0;
2012
+ for (const journey of Object.keys(status)) {
2013
+ journeyCount += 1;
2014
+ if (status[journey].status === 'error') journeyErrorCount += 1;
2015
+ for (const node of Object.keys(status[journey].nodes)) {
2016
+ nodeCount += 1;
2017
+ if (status[journey].nodes[node].status === 'error') nodeErrorCount += 1;
2018
+ }
2019
+ }
2020
+ stopProgressBar(
2021
+ `Deleted ${journeyCount - journeyErrorCount}/${journeyCount} journeys and ${
2022
+ nodeCount - nodeErrorCount
2023
+ }/${nodeCount} nodes.`
2024
+ );
2025
+ return status;
2026
+ }