@rockcarver/frodo-lib 2.0.0-1 → 2.0.0-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/cjs/api/ApiTypes.js.map +1 -1
  3. package/cjs/index.js +1 -105
  4. package/cjs/index.js.map +1 -1
  5. package/cjs/lib/FrodoLib.js +20 -20
  6. package/cjs/lib/FrodoLib.js.map +1 -1
  7. package/cjs/ops/AgentOps.js +8 -0
  8. package/cjs/ops/AgentOps.js.map +1 -1
  9. package/cjs/ops/AuthenticateOps.js +33 -6
  10. package/cjs/ops/AuthenticateOps.js.map +1 -1
  11. package/cjs/ops/CirclesOfTrustOps.js +36 -10
  12. package/cjs/ops/CirclesOfTrustOps.js.map +1 -1
  13. package/cjs/ops/ConnectionProfileOps.js +13 -126
  14. package/cjs/ops/ConnectionProfileOps.js.map +1 -1
  15. package/cjs/ops/IdmOps.js.map +1 -1
  16. package/cjs/ops/JourneyOps.js.map +1 -1
  17. package/cjs/ops/OAuth2OidcOps.js +84 -0
  18. package/cjs/ops/OAuth2OidcOps.js.map +1 -0
  19. package/cjs/ops/ResourceTypeOps.js +65 -49
  20. package/cjs/ops/ResourceTypeOps.js.map +1 -1
  21. package/cjs/ops/ServiceOps.js +3 -0
  22. package/cjs/ops/ServiceOps.js.map +1 -1
  23. package/cjs/ops/ThemeOps.js +28 -14
  24. package/cjs/ops/ThemeOps.js.map +1 -1
  25. package/cjs/ops/cloud/LogOps.js +3 -13
  26. package/cjs/ops/cloud/LogOps.js.map +1 -1
  27. package/cjs/ops/utils/Console.js +7 -7
  28. package/cjs/ops/utils/Console.js.map +1 -1
  29. package/cjs/ops/utils/ExportImportUtils.js +109 -0
  30. package/cjs/ops/utils/ExportImportUtils.js.map +1 -1
  31. package/cjs/ops/utils/ExportImportUtils.test.js.map +1 -1
  32. package/cjs/ops/utils/Version.js +29 -6
  33. package/cjs/ops/utils/Version.js.map +1 -1
  34. package/cjs/ops/utils/Version.test.js.map +1 -1
  35. package/cjs/shared/State.js +0 -2
  36. package/cjs/shared/State.js.map +1 -1
  37. package/esm/index.mjs +1 -59
  38. package/esm/lib/FrodoLib.mjs +19 -18
  39. package/esm/ops/AgentOps.mjs +8 -0
  40. package/esm/ops/AuthenticateOps.mjs +24 -4
  41. package/esm/ops/CirclesOfTrustOps.mjs +18 -2
  42. package/esm/ops/ConnectionProfileOps.mjs +1 -105
  43. package/esm/ops/OAuth2OidcOps.mjs +40 -0
  44. package/esm/ops/ResourceTypeOps.mjs +14 -4
  45. package/esm/ops/ServiceOps.mjs +3 -0
  46. package/esm/ops/ThemeOps.mjs +7 -7
  47. package/esm/ops/cloud/LogOps.mjs +3 -13
  48. package/esm/ops/utils/Console.mjs +8 -8
  49. package/esm/ops/utils/ExportImportUtils.mjs +101 -0
  50. package/esm/ops/utils/ExportImportUtils.test.mjs +15 -2
  51. package/esm/ops/utils/Version.mjs +21 -4
  52. package/esm/ops/utils/Version.test.mjs +4 -1
  53. package/esm/shared/State.mjs +0 -2
  54. package/package.json +1 -1
  55. package/types/api/ApiTypes.d.ts +11 -11
  56. package/types/api/ApiTypes.d.ts.map +1 -1
  57. package/types/index.d.ts +1 -48
  58. package/types/index.d.ts.map +1 -1
  59. package/types/lib/FrodoLib.d.ts +13 -9
  60. package/types/lib/FrodoLib.d.ts.map +1 -1
  61. package/types/ops/AgentOps.d.ts +5 -0
  62. package/types/ops/AgentOps.d.ts.map +1 -1
  63. package/types/ops/AuthenticateOps.d.ts +11 -1
  64. package/types/ops/AuthenticateOps.d.ts.map +1 -1
  65. package/types/ops/CirclesOfTrustOps.d.ts +16 -2
  66. package/types/ops/CirclesOfTrustOps.d.ts.map +1 -1
  67. package/types/ops/ConnectionProfileOps.d.ts +0 -19
  68. package/types/ops/ConnectionProfileOps.d.ts.map +1 -1
  69. package/types/ops/IdmOps.d.ts +1 -1
  70. package/types/ops/IdmOps.d.ts.map +1 -1
  71. package/types/ops/JourneyOps.d.ts +2 -2
  72. package/types/ops/JourneyOps.d.ts.map +1 -1
  73. package/types/ops/OAuth2OidcOps.d.ts +11 -0
  74. package/types/ops/OAuth2OidcOps.d.ts.map +1 -0
  75. package/types/ops/ResourceTypeOps.d.ts +3 -3
  76. package/types/ops/ResourceTypeOps.d.ts.map +1 -1
  77. package/types/ops/ServiceOps.d.ts +1 -0
  78. package/types/ops/ServiceOps.d.ts.map +1 -1
  79. package/types/ops/ThemeOps.d.ts +14 -14
  80. package/types/ops/ThemeOps.d.ts.map +1 -1
  81. package/types/ops/cloud/LogOps.d.ts +1 -1
  82. package/types/ops/cloud/LogOps.d.ts.map +1 -1
  83. package/types/ops/utils/Console.d.ts.map +1 -1
  84. package/types/ops/utils/ExportImportUtils.d.ts +43 -0
  85. package/types/ops/utils/ExportImportUtils.d.ts.map +1 -1
  86. package/types/ops/utils/Version.d.ts +16 -2
  87. package/types/ops/utils/Version.d.ts.map +1 -1
  88. package/types/shared/State.d.ts.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ops/ConnectionProfileOps.ts"],"names":[],"mappings":"AAYA,OAAO,EAA6C,MAAM,EAAE,MAAM,WAAW,CAAC;AAK9E,OAAO,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AAG5D,OAAO,KAAK,MAAM,iBAAiB,CAAC;AAEpC,MAAM,CAAC,OAAO,OAAO,oBAAoB;IACvC,KAAK,EAAE,KAAK,CAAC;gBACD,KAAK,EAAE,KAAK;IAIxB;;;OAGG;IACH,yBAAyB,IAAI,MAAM;IAInC;;;;;OAKG;IACH,sBAAsB,CACpB,kBAAkB,EAAE,wBAAwB,EAC5C,IAAI,EAAE,MAAM,GACX,gCAAgC,EAAE;IAIrC;;;;;OAKG;IACG,sBAAsB;IAI5B;;;;OAIG;IACG,0BAA0B,CAC9B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,0BAA0B,CAAC;IAItC;;;OAGG;IACG,oBAAoB,IAAI,OAAO,CAAC,0BAA0B,CAAC;IAIjE;;;;OAIG;IACG,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAI3D;;;OAGG;IACH,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAI3C;;;OAGG;IACG,oBAAoB,IAAI,OAAO,CAAC,yBAAyB,CAAC;CAGjE;AAMD,MAAM,WAAW,gCAAgC;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,6BAA6B,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvD,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,6BAA6B,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvD,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,wBAAwB;IACvC,CAAC,GAAG,EAAE,MAAM,GAAG,gCAAgC,CAAC;CACjD;AAKD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,GAAG,MAAM,CAM7E;AAiCD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,IAAY,EACZ,KAAK,GACN,EAAE;IACD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,KAAK,CAAC;CACd,QAsCA;AAyBD;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,iBAiDvE;AAED;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAAC,EAC/C,IAAI,EACJ,KAAK,GACN,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,KAAK,CAAC;CACd,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAqDtC;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,EACzC,KAAK,GACN,EAAE;IACD,KAAK,EAAE,KAAK,CAAC;CACd,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAEtC;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,EAC1C,IAAI,EACJ,KAAK,GACN,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,KAAK,CAAC;CACd,GAAG,OAAO,CAAC,OAAO,CAAC,CA2HnB;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,IAAI,EACJ,KAAK,GACN,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,KAAK,CAAC;CACd,QAoCA;AAED;;;;GAIG;AACH,wBAAsB,yBAAyB,CAAC,EAC9C,IAAI,EACJ,WAAW,EACX,KAAK,GACN,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;IACrB,KAAK,EAAE,KAAK,CAAC;CACd,iBAoDA;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,EACzC,KAAK,GACN,EAAE;IACD,KAAK,EAAE,KAAK,CAAC;CACd,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAsBrC","file":"ConnectionProfileOps.d.ts","sourcesContent":["import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport DataProtection from './utils/DataProtection';\nimport {\n createObjectTable,\n createTable,\n debugMessage,\n printMessage,\n verboseMessage,\n} from './utils/Console';\nimport { FRODO_CONNECTION_PROFILES_PATH_KEY } from '../storage/StaticStorage';\nimport { createJwkRsa, createJwks, getJwkRsaPublic, JwkRsa } from './JoseOps';\nimport {\n createServiceAccount,\n getServiceAccount,\n} from './cloud/ServiceAccountOps';\nimport { IdObjectSkeletonInterface } from '../api/ApiTypes';\nimport { saveJsonToFile } from './utils/ExportImportUtils';\nimport { isValidUrl } from './utils/OpsUtils';\nimport State from '../shared/State';\n\nexport default class ConnectionProfileOps {\n state: State;\n constructor(state: State) {\n this.state = state;\n }\n\n /**\n * Get connection profiles file name\n * @returns {string} connection profiles file name\n */\n getConnectionProfilesPath(): string {\n return getConnectionProfilesPath({ state: this.state });\n }\n\n /**\n * Find connection profiles\n * @param {ConnectionsFileInterface} connectionProfiles connection profile object\n * @param {string} host host url or unique substring\n * @returns {SecureConnectionProfileInterface[]} Array of connection profiles\n */\n findConnectionProfiles(\n connectionProfiles: ConnectionsFileInterface,\n host: string\n ): SecureConnectionProfileInterface[] {\n return findConnectionProfiles({ connectionProfiles, host });\n }\n\n /**\n * Initialize connection profiles\n *\n * This method is called from app.ts and runs before any of the message handlers are registered.\n * Therefore none of the Console message functions will produce any output.\n */\n async initConnectionProfiles() {\n initConnectionProfiles({ state: this.state });\n }\n\n /**\n * Get connection profile by host\n * @param {String} host host tenant host url or unique substring\n * @returns {Object} connection profile or null\n */\n async getConnectionProfileByHost(\n host: string\n ): Promise<ConnectionProfileInterface> {\n return getConnectionProfileByHost({ host, state: this.state });\n }\n\n /**\n * Get connection profile\n * @returns {Object} connection profile or null\n */\n async getConnectionProfile(): Promise<ConnectionProfileInterface> {\n return getConnectionProfile({ state: this.state });\n }\n\n /**\n * Save connection profile\n * @param {string} host host url for new profiles or unique substring for existing profiles\n * @returns {Promise<boolean>} true if the operation succeeded, false otherwise\n */\n async saveConnectionProfile(host: string): Promise<boolean> {\n return saveConnectionProfile({ host, state: this.state });\n }\n\n /**\n * Delete connection profile\n * @param {string} host host tenant host url or unique substring\n */\n deleteConnectionProfile(host: string): void {\n deleteConnectionProfile({ host, state: this.state });\n }\n\n /**\n * Create a new service account using auto-generated parameters\n * @returns {Promise<IdObjectSkeletonInterface>} A promise resolving to a service account object\n */\n async addNewServiceAccount(): Promise<IdObjectSkeletonInterface> {\n return addNewServiceAccount({ state: this.state });\n }\n}\n\nconst fileOptions = {\n indentation: 4,\n};\n\nexport interface SecureConnectionProfileInterface {\n tenant: string;\n username?: string | null;\n encodedPassword?: string | null;\n logApiKey?: string | null;\n encodedLogApiSecret?: string | null;\n authenticationService?: string | null;\n authenticationHeaderOverrides?: Record<string, string>;\n svcacctId?: string | null;\n encodedSvcacctJwk?: string | null;\n svcacctName?: string | null;\n}\n\nexport interface ConnectionProfileInterface {\n tenant: string;\n username?: string | null;\n password?: string | null;\n logApiKey?: string | null;\n logApiSecret?: string | null;\n authenticationService?: string | null;\n authenticationHeaderOverrides?: Record<string, string>;\n svcacctId?: string | null;\n svcacctJwk?: JwkRsa;\n svcacctName?: string | null;\n}\n\nexport interface ConnectionsFileInterface {\n [key: string]: SecureConnectionProfileInterface;\n}\n\nconst legacyProfileFilename = '.frodorc';\nconst newProfileFilename = 'Connections.json';\n\n/**\n * Get connection profiles file name\n * @param {State} state library state\n * @returns {String} connection profiles file name\n */\nexport function getConnectionProfilesPath({ state }: { state: State }): string {\n return (\n state.getConnectionProfilesPath() ||\n process.env[FRODO_CONNECTION_PROFILES_PATH_KEY] ||\n `${os.homedir()}/.frodo/${newProfileFilename}`\n );\n}\n\n/**\n * Find connection profiles\n * @param {ConnectionsFileInterface} connectionProfiles connection profile object\n * @param {string} host host url or unique substring\n * @param {State} state library state\n * @returns {SecureConnectionProfileInterface[]} Array of connection profiles\n */\nfunction findConnectionProfiles({\n connectionProfiles,\n host,\n}: {\n connectionProfiles: ConnectionsFileInterface;\n host: string;\n}): SecureConnectionProfileInterface[] {\n const profiles: SecureConnectionProfileInterface[] = [];\n for (const tenant in connectionProfiles) {\n debugMessage(\n `ConnectionProfileOps.findConnectionProfiles: tenant=${tenant}`\n );\n if (tenant.includes(host)) {\n debugMessage(\n `ConnectionProfileOps.findConnectionProfiles: '${host}' identifies '${tenant}', including in result set`\n );\n const foundProfile = { ...connectionProfiles[tenant] };\n foundProfile.tenant = tenant;\n profiles.push(foundProfile);\n }\n }\n return profiles;\n}\n\n/**\n * List connection profiles\n * @param {boolean} long Long list format with details\n * @param {State} state library state\n */\nexport function listConnectionProfiles({\n long = false,\n state,\n}: {\n long?: boolean;\n state: State;\n}) {\n const filename = getConnectionProfilesPath({ state });\n try {\n const data = fs.readFileSync(filename, 'utf8');\n const connectionsData = JSON.parse(data);\n if (Object.keys(connectionsData).length < 1) {\n printMessage(`No connections defined yet in ${filename}`, 'info');\n } else {\n if (long) {\n const table = createTable([\n 'Host',\n 'Service Account',\n 'Username',\n 'Log API Key',\n ]);\n Object.keys(connectionsData).forEach((c) => {\n table.push([\n c,\n connectionsData[c].svcacctName || connectionsData[c].svcacctId,\n connectionsData[c].username,\n connectionsData[c].logApiKey,\n ]);\n });\n printMessage(table.toString(), 'data');\n } else {\n Object.keys(connectionsData).forEach((c) => {\n printMessage(`${c}`, 'data');\n });\n // getUniqueNames(5, Object.keys(connectionsData));\n }\n printMessage(\n 'Any unique substring of a saved host can be used as the value for host parameter in all commands',\n 'info'\n );\n }\n } catch (e) {\n printMessage(`No connections found in ${filename} (${e.message})`, 'error');\n }\n}\n\n/**\n * Migrate from .frodorc to Connections.json\n */\nfunction migrateFromLegacyProfile() {\n const legacyPath = `${os.homedir()}/.frodo/${legacyProfileFilename}`;\n const newPath = `${os.homedir()}/.frodo/${newProfileFilename}`;\n if (!fs.existsSync(legacyPath) && !fs.existsSync(newPath)) {\n // no connections file (old or new), create empty new one\n fs.writeFileSync(\n newPath,\n JSON.stringify({}, null, fileOptions.indentation)\n );\n } else if (fs.existsSync(legacyPath) && !fs.existsSync(newPath)) {\n // old exists, new one does not - so copy old to new one\n fs.copyFileSync(legacyPath, newPath);\n // for now, just add a \"deprecated\" suffix. May delete the old file\n // in a future release\n fs.renameSync(legacyPath, `${legacyPath}.deprecated`);\n }\n // in other cases, where\n // (both old and new exist) OR (only new one exists) don't do anything\n}\n\n/**\n * Initialize connection profiles\n *\n * This method is called from app.ts and runs before any of the message handlers are registered.\n * Therefore none of the Console message functions will produce any output.\n * @param {State} state library state\n */\nexport async function initConnectionProfiles({ state }: { state: State }) {\n const dataProtection = new DataProtection(state.getMasterKeyPath());\n // create connections.json file if it doesn't exist\n const filename = getConnectionProfilesPath({ state });\n const folderName = path.dirname(filename);\n if (!fs.existsSync(folderName)) {\n fs.mkdirSync(folderName, { recursive: true });\n if (!fs.existsSync(filename)) {\n fs.writeFileSync(\n filename,\n JSON.stringify({}, null, fileOptions.indentation)\n );\n }\n }\n // encrypt the password and logApiSecret from clear text to aes-256-GCM\n else {\n migrateFromLegacyProfile();\n const data = fs.readFileSync(filename, 'utf8');\n const connectionsData: ConnectionsFileInterface = JSON.parse(data);\n let convert = false;\n for (const conn of Object.keys(connectionsData)) {\n if (connectionsData[conn]['password']) {\n convert = true;\n connectionsData[conn].encodedPassword = await dataProtection.encrypt(\n connectionsData[conn]['password']\n );\n delete connectionsData[conn]['password'];\n }\n if (connectionsData[conn]['logApiSecret']) {\n convert = true;\n connectionsData[conn].encodedLogApiSecret =\n await dataProtection.encrypt(connectionsData[conn]['logApiSecret']);\n delete connectionsData[conn]['logApiSecret'];\n }\n if (connectionsData[conn]['svcacctJwk']) {\n convert = true;\n connectionsData[conn].encodedSvcacctJwk = await dataProtection.encrypt(\n connectionsData[conn]['svcacctJwk']\n );\n delete connectionsData[conn]['svcacctJwk'];\n }\n }\n if (convert) {\n fs.writeFileSync(\n filename,\n JSON.stringify(connectionsData, null, fileOptions.indentation)\n );\n }\n }\n}\n\n/**\n * Get connection profile by host\n * @param {String} host host tenant host url or unique substring\n * @param {State} state library state\n * @returns {Object} connection profile or null\n */\nexport async function getConnectionProfileByHost({\n host,\n state,\n}: {\n host: string;\n state: State;\n}): Promise<ConnectionProfileInterface> {\n try {\n const dataProtection = new DataProtection(state.getMasterKeyPath());\n const filename = getConnectionProfilesPath({ state });\n const connectionsData = JSON.parse(fs.readFileSync(filename, 'utf8'));\n const profiles = findConnectionProfiles({\n connectionProfiles: connectionsData,\n host,\n });\n if (profiles.length == 0) {\n printMessage(\n `Profile for ${host} not found. Please specify credentials on command line`,\n 'error'\n );\n return null;\n }\n if (profiles.length > 1) {\n printMessage(`Multiple matching profiles found.`, 'error');\n profiles.forEach((p) => {\n printMessage(`- ${p.tenant}`, 'error');\n });\n printMessage(`Please specify a unique sub-string`, 'error');\n return null;\n }\n return {\n tenant: profiles[0].tenant,\n username: profiles[0].username ? profiles[0].username : null,\n password: profiles[0].encodedPassword\n ? await dataProtection.decrypt(profiles[0].encodedPassword)\n : null,\n logApiKey: profiles[0].logApiKey ? profiles[0].logApiKey : null,\n logApiSecret: profiles[0].encodedLogApiSecret\n ? await dataProtection.decrypt(profiles[0].encodedLogApiSecret)\n : null,\n authenticationService: profiles[0].authenticationService\n ? profiles[0].authenticationService\n : null,\n authenticationHeaderOverrides: profiles[0].authenticationHeaderOverrides\n ? profiles[0].authenticationHeaderOverrides\n : {},\n svcacctName: profiles[0].svcacctName ? profiles[0].svcacctName : null,\n svcacctId: profiles[0].svcacctId ? profiles[0].svcacctId : null,\n svcacctJwk: profiles[0].encodedSvcacctJwk\n ? await dataProtection.decrypt(profiles[0].encodedSvcacctJwk)\n : null,\n };\n } catch (e) {\n printMessage(\n `Can not read saved connection info, please specify credentials on command line: ${e}`,\n 'error'\n );\n return null;\n }\n}\n\n/**\n * Get connection profile\n * @returns {Object} connection profile or null\n */\nexport async function getConnectionProfile({\n state,\n}: {\n state: State;\n}): Promise<ConnectionProfileInterface> {\n return getConnectionProfileByHost({ host: state.getHost(), state });\n}\n\n/**\n * Save connection profile\n * @param {string} host host url for new profiles or unique substring for existing profiles\n * @returns {Promise<boolean>} true if the operation succeeded, false otherwise\n */\nexport async function saveConnectionProfile({\n host,\n state,\n}: {\n host: string;\n state: State;\n}): Promise<boolean> {\n debugMessage(`ConnectionProfileOps.saveConnectionProfile: start`);\n const dataProtection = new DataProtection(state.getMasterKeyPath());\n const filename = getConnectionProfilesPath({ state });\n debugMessage(`Saving connection profile in ${filename}`);\n let profiles: ConnectionsFileInterface = {};\n let profile: SecureConnectionProfileInterface = { tenant: '' };\n try {\n fs.statSync(filename);\n const data = fs.readFileSync(filename, 'utf8');\n profiles = JSON.parse(data);\n\n // find tenant\n const found = findConnectionProfiles({\n connectionProfiles: profiles,\n host,\n });\n\n // replace tenant in session with real tenant url if necessary\n if (found.length === 1) {\n profile = found[0];\n state.setHost(profile.tenant);\n verboseMessage(`Existing profile: ${profile.tenant}`);\n debugMessage(profile);\n }\n\n // connection profile not found, validate host is a real URL\n if (found.length === 0) {\n if (isValidUrl(host)) {\n state.setHost(host);\n debugMessage(`New profile: ${host}`);\n } else {\n printMessage(\n `No existing profile found matching '${host}'. Provide a valid URL as the host argument to create a new profile.`,\n 'error'\n );\n debugMessage(`ConnectionProfileOps.saveConnectionProfile: end [false]`);\n return false;\n }\n }\n } catch (error) {\n debugMessage(`New profiles file ${filename} with new profile ${host}`);\n }\n\n // user account\n if (state.getUsername()) profile.username = state.getUsername();\n if (state.getPassword())\n profile.encodedPassword = await dataProtection.encrypt(state.getPassword());\n\n // log API\n if (state.getLogApiKey()) profile.logApiKey = state.getLogApiKey();\n if (state.getLogApiSecret())\n profile.encodedLogApiSecret = await dataProtection.encrypt(\n state.getLogApiSecret()\n );\n\n // service account\n if (state.getServiceAccountId()) {\n profile.svcacctId = state.getServiceAccountId();\n profile.svcacctName = (\n await getServiceAccount({\n serviceAccountId: state.getServiceAccountId(),\n state,\n })\n ).name;\n }\n if (state.getServiceAccountJwk())\n profile.encodedSvcacctJwk = await dataProtection.encrypt(\n state.getServiceAccountJwk()\n );\n // update existing service account profile\n if (profile.svcacctId && !profile.svcacctName) {\n profile.svcacctName = (\n await getServiceAccount({ serviceAccountId: profile.svcacctId, state })\n ).name;\n debugMessage(\n `ConnectionProfileOps.saveConnectionProfile: added missing service account name`\n );\n }\n\n // advanced settings\n if (state.getAuthenticationService()) {\n profile.authenticationService = state.getAuthenticationService();\n printMessage(\n 'Advanced setting: Authentication Service: ' +\n state.getAuthenticationService(),\n 'info'\n );\n }\n if (\n state.getAuthenticationHeaderOverrides() &&\n Object.entries(state.getAuthenticationHeaderOverrides()).length\n ) {\n profile.authenticationHeaderOverrides =\n state.getAuthenticationHeaderOverrides();\n printMessage('Advanced setting: Authentication Header Overrides: ', 'info');\n printMessage(state.getAuthenticationHeaderOverrides(), 'info');\n }\n\n // remove the helper key 'tenant'\n delete profile.tenant;\n\n // update profiles\n profiles[state.getHost()] = profile;\n\n // sort profiles\n const orderedProfiles = Object.keys(profiles)\n .sort()\n .reduce((obj, key) => {\n obj[key] = profiles[key];\n return obj;\n }, {});\n\n // save profiles\n saveJsonToFile({\n data: orderedProfiles,\n filename,\n includeMeta: false,\n state,\n });\n verboseMessage(`Saved connection profile ${state.getHost()} in ${filename}`);\n debugMessage(`ConnectionProfileOps.saveConnectionProfile: end [true]`);\n return true;\n}\n\n/**\n * Delete connection profile\n * @param {String} host host tenant host url or unique substring\n */\nexport function deleteConnectionProfile({\n host,\n state,\n}: {\n host: string;\n state: State;\n}) {\n const filename = getConnectionProfilesPath({ state });\n let connectionsData: ConnectionsFileInterface = {};\n fs.stat(filename, (err) => {\n if (err == null) {\n const data = fs.readFileSync(filename, 'utf8');\n connectionsData = JSON.parse(data);\n const profiles = findConnectionProfiles({\n connectionProfiles: connectionsData,\n host,\n });\n if (profiles.length == 1) {\n delete connectionsData[profiles[0].tenant];\n fs.writeFileSync(filename, JSON.stringify(connectionsData, null, 2));\n printMessage(`Deleted connection profile ${profiles[0].tenant}`);\n } else {\n if (profiles.length > 1) {\n printMessage(`Multiple matching profiles found.`, 'error');\n profiles.forEach((p) => {\n printMessage(`- ${p.tenant}`, 'error');\n });\n printMessage(`Please specify a unique sub-string`, 'error');\n return null;\n } else {\n printMessage(`No connection profile ${host} found`);\n }\n }\n } else if (err.code === 'ENOENT') {\n printMessage(`Connection profile file ${filename} not found`);\n } else {\n printMessage(\n `Error in deleting connection profile: ${err.code}`,\n 'error'\n );\n }\n });\n}\n\n/**\n * Describe connection profile\n * @param {string} host Host URL or unique substring\n * @param {boolean} showSecrets Whether secrets should be shown in clear text or not\n */\nexport async function describeConnectionProfile({\n host,\n showSecrets,\n state,\n}: {\n host: string;\n showSecrets: boolean;\n state: State;\n}) {\n debugMessage(`ConnectionProfileOps.describeConnectionProfile: start`);\n const profile = await getConnectionProfileByHost({ host, state });\n if (profile) {\n debugMessage(profile);\n const present = '[present]';\n const jwk = profile.svcacctJwk;\n if (!showSecrets) {\n if (profile.password) profile.password = present;\n if (profile.logApiSecret) profile.logApiSecret = present;\n if (profile.svcacctJwk) (profile as unknown)['svcacctJwk'] = present;\n }\n if (!profile.username) {\n delete profile.username;\n delete profile.password;\n }\n if (!profile.logApiKey) {\n delete profile.logApiKey;\n delete profile.logApiSecret;\n }\n if (!profile.svcacctId) {\n delete profile.svcacctId;\n delete profile.svcacctJwk;\n delete profile.svcacctName;\n }\n if (showSecrets && jwk) {\n (profile as unknown)['svcacctJwk'] = 'see below';\n }\n if (!profile.authenticationService) {\n delete profile.authenticationService;\n }\n const keyMap = {\n tenant: 'Host',\n username: 'Username',\n password: 'Password',\n logApiKey: 'Log API Key',\n logApiSecret: 'Log API Secret',\n authenticationService: 'Authentication Service',\n authenticationHeaderOverrides: 'Authentication Header Overrides',\n svcacctName: 'Service Account Name',\n svcacctId: 'Service Account Id',\n svcacctJwk: 'Service Account JWK',\n };\n const table = createObjectTable(profile, keyMap);\n printMessage(table.toString(), 'data');\n if (showSecrets && jwk) {\n printMessage(jwk, 'data');\n }\n } else {\n printMessage(`No connection profile ${host} found`);\n }\n debugMessage(`ConnectionProfileOps.describeConnectionProfile: end`);\n}\n\n/**\n * Create a new service account using auto-generated parameters\n * @returns {Promise<IdObjectSkeletonInterface>} A promise resolving to a service account object\n */\nexport async function addNewServiceAccount({\n state,\n}: {\n state: State;\n}): Promise<IdObjectSkeletonInterface> {\n debugMessage(`ConnectionProfileOps.addNewServiceAccount: start`);\n const name = `Frodo-SA-${new Date().getTime()}`;\n debugMessage(`ConnectionProfileOps.addNewServiceAccount: name=${name}...`);\n const description = `${state.getUsername()}'s Frodo Service Account`;\n const scope = ['fr:am:*', 'fr:idm:*', 'fr:idc:esv:*'];\n const jwkPrivate = await createJwkRsa();\n const jwkPublic = await getJwkRsaPublic(jwkPrivate);\n const jwks = createJwks(jwkPublic);\n const sa = await createServiceAccount({\n name,\n description,\n accountStatus: 'Active',\n scopes: scope,\n jwks,\n state,\n });\n debugMessage(`ConnectionProfileOps.addNewServiceAccount: id=${sa._id}`);\n state.setServiceAccountId(sa._id);\n state.setServiceAccountJwk(jwkPrivate);\n debugMessage(`ConnectionProfileOps.addNewServiceAccount: end`);\n return sa;\n}\n"]}
1
+ {"version":3,"sources":["../src/ops/ConnectionProfileOps.ts"],"names":[],"mappings":"AAMA,OAAO,EAA6C,MAAM,EAAE,MAAM,WAAW,CAAC;AAK9E,OAAO,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AAG5D,OAAO,KAAK,MAAM,iBAAiB,CAAC;AAEpC,MAAM,CAAC,OAAO,OAAO,oBAAoB;IACvC,KAAK,EAAE,KAAK,CAAC;gBACD,KAAK,EAAE,KAAK;IAIxB;;;OAGG;IACH,yBAAyB,IAAI,MAAM;IAInC;;;;;OAKG;IACH,sBAAsB,CACpB,kBAAkB,EAAE,wBAAwB,EAC5C,IAAI,EAAE,MAAM,GACX,gCAAgC,EAAE;IAIrC;;;;;OAKG;IACG,sBAAsB;IAI5B;;;;OAIG;IACG,0BAA0B,CAC9B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,0BAA0B,CAAC;IAItC;;;OAGG;IACG,oBAAoB,IAAI,OAAO,CAAC,0BAA0B,CAAC;IAIjE;;;;OAIG;IACG,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAI3D;;;OAGG;IACH,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAI3C;;;OAGG;IACG,oBAAoB,IAAI,OAAO,CAAC,yBAAyB,CAAC;CAGjE;AAMD,MAAM,WAAW,gCAAgC;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,6BAA6B,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvD,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,6BAA6B,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvD,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,wBAAwB;IACvC,CAAC,GAAG,EAAE,MAAM,GAAG,gCAAgC,CAAC;CACjD;AAKD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,GAAG,MAAM,CAM7E;AAwDD;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,iBAiDvE;AAED;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAAC,EAC/C,IAAI,EACJ,KAAK,GACN,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,KAAK,CAAC;CACd,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAqDtC;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,EACzC,KAAK,GACN,EAAE;IACD,KAAK,EAAE,KAAK,CAAC;CACd,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAEtC;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,EAC1C,IAAI,EACJ,KAAK,GACN,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,KAAK,CAAC;CACd,GAAG,OAAO,CAAC,OAAO,CAAC,CA2HnB;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,IAAI,EACJ,KAAK,GACN,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,KAAK,CAAC;CACd,QAoCA;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,EACzC,KAAK,GACN,EAAE;IACD,KAAK,EAAE,KAAK,CAAC;CACd,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAsBrC","file":"ConnectionProfileOps.d.ts","sourcesContent":["import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport DataProtection from './utils/DataProtection';\nimport { debugMessage, printMessage, verboseMessage } from './utils/Console';\nimport { FRODO_CONNECTION_PROFILES_PATH_KEY } from '../storage/StaticStorage';\nimport { createJwkRsa, createJwks, getJwkRsaPublic, JwkRsa } from './JoseOps';\nimport {\n createServiceAccount,\n getServiceAccount,\n} from './cloud/ServiceAccountOps';\nimport { IdObjectSkeletonInterface } from '../api/ApiTypes';\nimport { saveJsonToFile } from './utils/ExportImportUtils';\nimport { isValidUrl } from './utils/OpsUtils';\nimport State from '../shared/State';\n\nexport default class ConnectionProfileOps {\n state: State;\n constructor(state: State) {\n this.state = state;\n }\n\n /**\n * Get connection profiles file name\n * @returns {string} connection profiles file name\n */\n getConnectionProfilesPath(): string {\n return getConnectionProfilesPath({ state: this.state });\n }\n\n /**\n * Find connection profiles\n * @param {ConnectionsFileInterface} connectionProfiles connection profile object\n * @param {string} host host url or unique substring\n * @returns {SecureConnectionProfileInterface[]} Array of connection profiles\n */\n findConnectionProfiles(\n connectionProfiles: ConnectionsFileInterface,\n host: string\n ): SecureConnectionProfileInterface[] {\n return findConnectionProfiles({ connectionProfiles, host });\n }\n\n /**\n * Initialize connection profiles\n *\n * This method is called from app.ts and runs before any of the message handlers are registered.\n * Therefore none of the Console message functions will produce any output.\n */\n async initConnectionProfiles() {\n initConnectionProfiles({ state: this.state });\n }\n\n /**\n * Get connection profile by host\n * @param {String} host host tenant host url or unique substring\n * @returns {Object} connection profile or null\n */\n async getConnectionProfileByHost(\n host: string\n ): Promise<ConnectionProfileInterface> {\n return getConnectionProfileByHost({ host, state: this.state });\n }\n\n /**\n * Get connection profile\n * @returns {Object} connection profile or null\n */\n async getConnectionProfile(): Promise<ConnectionProfileInterface> {\n return getConnectionProfile({ state: this.state });\n }\n\n /**\n * Save connection profile\n * @param {string} host host url for new profiles or unique substring for existing profiles\n * @returns {Promise<boolean>} true if the operation succeeded, false otherwise\n */\n async saveConnectionProfile(host: string): Promise<boolean> {\n return saveConnectionProfile({ host, state: this.state });\n }\n\n /**\n * Delete connection profile\n * @param {string} host host tenant host url or unique substring\n */\n deleteConnectionProfile(host: string): void {\n deleteConnectionProfile({ host, state: this.state });\n }\n\n /**\n * Create a new service account using auto-generated parameters\n * @returns {Promise<IdObjectSkeletonInterface>} A promise resolving to a service account object\n */\n async addNewServiceAccount(): Promise<IdObjectSkeletonInterface> {\n return addNewServiceAccount({ state: this.state });\n }\n}\n\nconst fileOptions = {\n indentation: 4,\n};\n\nexport interface SecureConnectionProfileInterface {\n tenant: string;\n username?: string | null;\n encodedPassword?: string | null;\n logApiKey?: string | null;\n encodedLogApiSecret?: string | null;\n authenticationService?: string | null;\n authenticationHeaderOverrides?: Record<string, string>;\n svcacctId?: string | null;\n encodedSvcacctJwk?: string | null;\n svcacctName?: string | null;\n}\n\nexport interface ConnectionProfileInterface {\n tenant: string;\n username?: string | null;\n password?: string | null;\n logApiKey?: string | null;\n logApiSecret?: string | null;\n authenticationService?: string | null;\n authenticationHeaderOverrides?: Record<string, string>;\n svcacctId?: string | null;\n svcacctJwk?: JwkRsa;\n svcacctName?: string | null;\n}\n\nexport interface ConnectionsFileInterface {\n [key: string]: SecureConnectionProfileInterface;\n}\n\nconst legacyProfileFilename = '.frodorc';\nconst newProfileFilename = 'Connections.json';\n\n/**\n * Get connection profiles file name\n * @param {State} state library state\n * @returns {String} connection profiles file name\n */\nexport function getConnectionProfilesPath({ state }: { state: State }): string {\n return (\n state.getConnectionProfilesPath() ||\n process.env[FRODO_CONNECTION_PROFILES_PATH_KEY] ||\n `${os.homedir()}/.frodo/${newProfileFilename}`\n );\n}\n\n/**\n * Find connection profiles\n * @param {ConnectionsFileInterface} connectionProfiles connection profile object\n * @param {string} host host url or unique substring\n * @param {State} state library state\n * @returns {SecureConnectionProfileInterface[]} Array of connection profiles\n */\nfunction findConnectionProfiles({\n connectionProfiles,\n host,\n}: {\n connectionProfiles: ConnectionsFileInterface;\n host: string;\n}): SecureConnectionProfileInterface[] {\n const profiles: SecureConnectionProfileInterface[] = [];\n for (const tenant in connectionProfiles) {\n debugMessage(\n `ConnectionProfileOps.findConnectionProfiles: tenant=${tenant}`\n );\n if (tenant.includes(host)) {\n debugMessage(\n `ConnectionProfileOps.findConnectionProfiles: '${host}' identifies '${tenant}', including in result set`\n );\n const foundProfile = { ...connectionProfiles[tenant] };\n foundProfile.tenant = tenant;\n profiles.push(foundProfile);\n }\n }\n return profiles;\n}\n\n/**\n * Migrate from .frodorc to Connections.json\n */\nfunction migrateFromLegacyProfile() {\n const legacyPath = `${os.homedir()}/.frodo/${legacyProfileFilename}`;\n const newPath = `${os.homedir()}/.frodo/${newProfileFilename}`;\n if (!fs.existsSync(legacyPath) && !fs.existsSync(newPath)) {\n // no connections file (old or new), create empty new one\n fs.writeFileSync(\n newPath,\n JSON.stringify({}, null, fileOptions.indentation)\n );\n } else if (fs.existsSync(legacyPath) && !fs.existsSync(newPath)) {\n // old exists, new one does not - so copy old to new one\n fs.copyFileSync(legacyPath, newPath);\n // for now, just add a \"deprecated\" suffix. May delete the old file\n // in a future release\n fs.renameSync(legacyPath, `${legacyPath}.deprecated`);\n }\n // in other cases, where\n // (both old and new exist) OR (only new one exists) don't do anything\n}\n\n/**\n * Initialize connection profiles\n *\n * This method is called from app.ts and runs before any of the message handlers are registered.\n * Therefore none of the Console message functions will produce any output.\n * @param {State} state library state\n */\nexport async function initConnectionProfiles({ state }: { state: State }) {\n const dataProtection = new DataProtection(state.getMasterKeyPath());\n // create connections.json file if it doesn't exist\n const filename = getConnectionProfilesPath({ state });\n const folderName = path.dirname(filename);\n if (!fs.existsSync(folderName)) {\n fs.mkdirSync(folderName, { recursive: true });\n if (!fs.existsSync(filename)) {\n fs.writeFileSync(\n filename,\n JSON.stringify({}, null, fileOptions.indentation)\n );\n }\n }\n // encrypt the password and logApiSecret from clear text to aes-256-GCM\n else {\n migrateFromLegacyProfile();\n const data = fs.readFileSync(filename, 'utf8');\n const connectionsData: ConnectionsFileInterface = JSON.parse(data);\n let convert = false;\n for (const conn of Object.keys(connectionsData)) {\n if (connectionsData[conn]['password']) {\n convert = true;\n connectionsData[conn].encodedPassword = await dataProtection.encrypt(\n connectionsData[conn]['password']\n );\n delete connectionsData[conn]['password'];\n }\n if (connectionsData[conn]['logApiSecret']) {\n convert = true;\n connectionsData[conn].encodedLogApiSecret =\n await dataProtection.encrypt(connectionsData[conn]['logApiSecret']);\n delete connectionsData[conn]['logApiSecret'];\n }\n if (connectionsData[conn]['svcacctJwk']) {\n convert = true;\n connectionsData[conn].encodedSvcacctJwk = await dataProtection.encrypt(\n connectionsData[conn]['svcacctJwk']\n );\n delete connectionsData[conn]['svcacctJwk'];\n }\n }\n if (convert) {\n fs.writeFileSync(\n filename,\n JSON.stringify(connectionsData, null, fileOptions.indentation)\n );\n }\n }\n}\n\n/**\n * Get connection profile by host\n * @param {String} host host tenant host url or unique substring\n * @param {State} state library state\n * @returns {Object} connection profile or null\n */\nexport async function getConnectionProfileByHost({\n host,\n state,\n}: {\n host: string;\n state: State;\n}): Promise<ConnectionProfileInterface> {\n try {\n const dataProtection = new DataProtection(state.getMasterKeyPath());\n const filename = getConnectionProfilesPath({ state });\n const connectionsData = JSON.parse(fs.readFileSync(filename, 'utf8'));\n const profiles = findConnectionProfiles({\n connectionProfiles: connectionsData,\n host,\n });\n if (profiles.length == 0) {\n printMessage(\n `Profile for ${host} not found. Please specify credentials on command line`,\n 'error'\n );\n return null;\n }\n if (profiles.length > 1) {\n printMessage(`Multiple matching profiles found.`, 'error');\n profiles.forEach((p) => {\n printMessage(`- ${p.tenant}`, 'error');\n });\n printMessage(`Please specify a unique sub-string`, 'error');\n return null;\n }\n return {\n tenant: profiles[0].tenant,\n username: profiles[0].username ? profiles[0].username : null,\n password: profiles[0].encodedPassword\n ? await dataProtection.decrypt(profiles[0].encodedPassword)\n : null,\n logApiKey: profiles[0].logApiKey ? profiles[0].logApiKey : null,\n logApiSecret: profiles[0].encodedLogApiSecret\n ? await dataProtection.decrypt(profiles[0].encodedLogApiSecret)\n : null,\n authenticationService: profiles[0].authenticationService\n ? profiles[0].authenticationService\n : null,\n authenticationHeaderOverrides: profiles[0].authenticationHeaderOverrides\n ? profiles[0].authenticationHeaderOverrides\n : {},\n svcacctName: profiles[0].svcacctName ? profiles[0].svcacctName : null,\n svcacctId: profiles[0].svcacctId ? profiles[0].svcacctId : null,\n svcacctJwk: profiles[0].encodedSvcacctJwk\n ? await dataProtection.decrypt(profiles[0].encodedSvcacctJwk)\n : null,\n };\n } catch (e) {\n printMessage(\n `Can not read saved connection info, please specify credentials on command line: ${e}`,\n 'error'\n );\n return null;\n }\n}\n\n/**\n * Get connection profile\n * @returns {Object} connection profile or null\n */\nexport async function getConnectionProfile({\n state,\n}: {\n state: State;\n}): Promise<ConnectionProfileInterface> {\n return getConnectionProfileByHost({ host: state.getHost(), state });\n}\n\n/**\n * Save connection profile\n * @param {string} host host url for new profiles or unique substring for existing profiles\n * @returns {Promise<boolean>} true if the operation succeeded, false otherwise\n */\nexport async function saveConnectionProfile({\n host,\n state,\n}: {\n host: string;\n state: State;\n}): Promise<boolean> {\n debugMessage(`ConnectionProfileOps.saveConnectionProfile: start`);\n const dataProtection = new DataProtection(state.getMasterKeyPath());\n const filename = getConnectionProfilesPath({ state });\n debugMessage(`Saving connection profile in ${filename}`);\n let profiles: ConnectionsFileInterface = {};\n let profile: SecureConnectionProfileInterface = { tenant: '' };\n try {\n fs.statSync(filename);\n const data = fs.readFileSync(filename, 'utf8');\n profiles = JSON.parse(data);\n\n // find tenant\n const found = findConnectionProfiles({\n connectionProfiles: profiles,\n host,\n });\n\n // replace tenant in session with real tenant url if necessary\n if (found.length === 1) {\n profile = found[0];\n state.setHost(profile.tenant);\n verboseMessage(`Existing profile: ${profile.tenant}`);\n debugMessage(profile);\n }\n\n // connection profile not found, validate host is a real URL\n if (found.length === 0) {\n if (isValidUrl(host)) {\n state.setHost(host);\n debugMessage(`New profile: ${host}`);\n } else {\n printMessage(\n `No existing profile found matching '${host}'. Provide a valid URL as the host argument to create a new profile.`,\n 'error'\n );\n debugMessage(`ConnectionProfileOps.saveConnectionProfile: end [false]`);\n return false;\n }\n }\n } catch (error) {\n debugMessage(`New profiles file ${filename} with new profile ${host}`);\n }\n\n // user account\n if (state.getUsername()) profile.username = state.getUsername();\n if (state.getPassword())\n profile.encodedPassword = await dataProtection.encrypt(state.getPassword());\n\n // log API\n if (state.getLogApiKey()) profile.logApiKey = state.getLogApiKey();\n if (state.getLogApiSecret())\n profile.encodedLogApiSecret = await dataProtection.encrypt(\n state.getLogApiSecret()\n );\n\n // service account\n if (state.getServiceAccountId()) {\n profile.svcacctId = state.getServiceAccountId();\n profile.svcacctName = (\n await getServiceAccount({\n serviceAccountId: state.getServiceAccountId(),\n state,\n })\n ).name;\n }\n if (state.getServiceAccountJwk())\n profile.encodedSvcacctJwk = await dataProtection.encrypt(\n state.getServiceAccountJwk()\n );\n // update existing service account profile\n if (profile.svcacctId && !profile.svcacctName) {\n profile.svcacctName = (\n await getServiceAccount({ serviceAccountId: profile.svcacctId, state })\n ).name;\n debugMessage(\n `ConnectionProfileOps.saveConnectionProfile: added missing service account name`\n );\n }\n\n // advanced settings\n if (state.getAuthenticationService()) {\n profile.authenticationService = state.getAuthenticationService();\n printMessage(\n 'Advanced setting: Authentication Service: ' +\n state.getAuthenticationService(),\n 'info'\n );\n }\n if (\n state.getAuthenticationHeaderOverrides() &&\n Object.entries(state.getAuthenticationHeaderOverrides()).length\n ) {\n profile.authenticationHeaderOverrides =\n state.getAuthenticationHeaderOverrides();\n printMessage('Advanced setting: Authentication Header Overrides: ', 'info');\n printMessage(state.getAuthenticationHeaderOverrides(), 'info');\n }\n\n // remove the helper key 'tenant'\n delete profile.tenant;\n\n // update profiles\n profiles[state.getHost()] = profile;\n\n // sort profiles\n const orderedProfiles = Object.keys(profiles)\n .sort()\n .reduce((obj, key) => {\n obj[key] = profiles[key];\n return obj;\n }, {});\n\n // save profiles\n saveJsonToFile({\n data: orderedProfiles,\n filename,\n includeMeta: false,\n state,\n });\n verboseMessage(`Saved connection profile ${state.getHost()} in ${filename}`);\n debugMessage(`ConnectionProfileOps.saveConnectionProfile: end [true]`);\n return true;\n}\n\n/**\n * Delete connection profile\n * @param {String} host host tenant host url or unique substring\n */\nexport function deleteConnectionProfile({\n host,\n state,\n}: {\n host: string;\n state: State;\n}) {\n const filename = getConnectionProfilesPath({ state });\n let connectionsData: ConnectionsFileInterface = {};\n fs.stat(filename, (err) => {\n if (err == null) {\n const data = fs.readFileSync(filename, 'utf8');\n connectionsData = JSON.parse(data);\n const profiles = findConnectionProfiles({\n connectionProfiles: connectionsData,\n host,\n });\n if (profiles.length == 1) {\n delete connectionsData[profiles[0].tenant];\n fs.writeFileSync(filename, JSON.stringify(connectionsData, null, 2));\n printMessage(`Deleted connection profile ${profiles[0].tenant}`);\n } else {\n if (profiles.length > 1) {\n printMessage(`Multiple matching profiles found.`, 'error');\n profiles.forEach((p) => {\n printMessage(`- ${p.tenant}`, 'error');\n });\n printMessage(`Please specify a unique sub-string`, 'error');\n return null;\n } else {\n printMessage(`No connection profile ${host} found`);\n }\n }\n } else if (err.code === 'ENOENT') {\n printMessage(`Connection profile file ${filename} not found`);\n } else {\n printMessage(\n `Error in deleting connection profile: ${err.code}`,\n 'error'\n );\n }\n });\n}\n\n/**\n * Create a new service account using auto-generated parameters\n * @returns {Promise<IdObjectSkeletonInterface>} A promise resolving to a service account object\n */\nexport async function addNewServiceAccount({\n state,\n}: {\n state: State;\n}): Promise<IdObjectSkeletonInterface> {\n debugMessage(`ConnectionProfileOps.addNewServiceAccount: start`);\n const name = `Frodo-SA-${new Date().getTime()}`;\n debugMessage(`ConnectionProfileOps.addNewServiceAccount: name=${name}...`);\n const description = `${state.getUsername()}'s Frodo Service Account`;\n const scope = ['fr:am:*', 'fr:idm:*', 'fr:idc:esv:*'];\n const jwkPrivate = await createJwkRsa();\n const jwkPublic = await getJwkRsaPublic(jwkPrivate);\n const jwks = createJwks(jwkPublic);\n const sa = await createServiceAccount({\n name,\n description,\n accountStatus: 'Active',\n scopes: scope,\n jwks,\n state,\n });\n debugMessage(`ConnectionProfileOps.addNewServiceAccount: id=${sa._id}`);\n state.setServiceAccountId(sa._id);\n state.setServiceAccountJwk(jwkPrivate);\n debugMessage(`ConnectionProfileOps.addNewServiceAccount: end`);\n return sa;\n}\n"]}
@@ -8,7 +8,7 @@ export default class IdmOps {
8
8
  getAllConfigEntities(): Promise<any>;
9
9
  getConfigEntitiesByType(type: string): Promise<any>;
10
10
  getConfigEntity(entityId: string): Promise<any>;
11
- putConfigEntity(entityId: string, entityData: NoIdObjectSkeletonInterface): Promise<any>;
11
+ putConfigEntity(entityId: string, entityData: NoIdObjectSkeletonInterface | string): Promise<any>;
12
12
  /**
13
13
  * Query managed objects
14
14
  * @param {string} type managed object type
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ops/IdmOps.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAC3B,WAAW,EACZ,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,eAAe,EACf,uBAAuB,EACvB,4BAA4B,EAC7B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAEL,8BAA8B,EAC/B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,MAAM,iBAAiB,CAAC;AAEpC,MAAM,CAAC,OAAO,OAAO,MAAM;IACzB,KAAK,EAAE,KAAK,CAAC;gBACD,KAAK,EAAE,KAAK;IAIxB,oBAAoB;IAIpB,uBAAuB,CAAC,IAAI,EAAE,MAAM;IAIpC,eAAe,CAAC,QAAQ,EAAE,MAAM;IAIhC,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,2BAA2B;IAIzE;;;;;;OAMG;IACH,4BAA4B,CAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EAAE,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,WAAW,CAAC,yBAAyB,CAAC,CAAC;IASlD;;;OAGG;IACG,oBAAoB,IAAI,OAAO,CAAC,8BAA8B,EAAE,CAAC;CAGxE;AAED,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,eAAe,EACf,eAAe,EACf,4BAA4B,GAC7B,CAAC;AAEF;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,EACzC,KAAK,GACN,EAAE;IACD,KAAK,EAAE,KAAK,CAAC;CACd,GAAG,OAAO,CAAC,8BAA8B,EAAE,CAAC,CAG5C","file":"IdmOps.d.ts","sourcesContent":["import {\n IdObjectSkeletonInterface,\n NoIdObjectSkeletonInterface,\n PagedResult,\n} from '../api/ApiTypes';\nimport {\n getAllConfigEntities,\n getConfigEntity,\n putConfigEntity,\n getConfigEntitiesByType,\n queryAllManagedObjectsByType,\n} from '../api/IdmConfigApi';\nimport {\n testConnectorServers as _testConnectorServers,\n ConnectorServerStatusInterface,\n} from '../api/IdmSystemApi';\nimport State from '../shared/State';\n\nexport default class IdmOps {\n state: State;\n constructor(state: State) {\n this.state = state;\n }\n\n getAllConfigEntities() {\n return getAllConfigEntities({ state: this.state });\n }\n\n getConfigEntitiesByType(type: string) {\n return getConfigEntitiesByType({ type, state: this.state });\n }\n\n getConfigEntity(entityId: string) {\n return getConfigEntity({ entityId, state: this.state });\n }\n\n putConfigEntity(entityId: string, entityData: NoIdObjectSkeletonInterface) {\n return putConfigEntity({ entityId, entityData, state: this.state });\n }\n\n /**\n * Query managed objects\n * @param {string} type managed object type\n * @param {string[]} fields fields to retrieve\n * @param {string} pageCookie paged results cookie\n * @returns {Promise<PagedResult<IdObjectSkeletonInterface>>} a promise that resolves to managed objects of the desired type\n */\n queryAllManagedObjectsByType(\n type: string,\n fields: string[],\n pageCookie: string\n ): Promise<PagedResult<IdObjectSkeletonInterface>> {\n return queryAllManagedObjectsByType({\n type,\n fields,\n pageCookie,\n state: this.state,\n });\n }\n\n /**\n * Test connector servers\n * @returns {Promise<ConnectorServerStatusInterface[]>} a promise that resolves to an array of ConnectorServerStatusInterface objects\n */\n async testConnectorServers(): Promise<ConnectorServerStatusInterface[]> {\n return testConnectorServers({ state: this.state });\n }\n}\n\nexport {\n getAllConfigEntities,\n getConfigEntitiesByType,\n getConfigEntity,\n putConfigEntity,\n queryAllManagedObjectsByType,\n};\n\n/**\n * Test connector servers\n * @returns {Promise<ConnectorServerStatusInterface[]>} a promise that resolves to an array of ConnectorServerStatusInterface objects\n */\nexport async function testConnectorServers({\n state,\n}: {\n state: State;\n}): Promise<ConnectorServerStatusInterface[]> {\n const response = await _testConnectorServers({ state });\n return response.openicf;\n}\n"]}
1
+ {"version":3,"sources":["../src/ops/IdmOps.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAC3B,WAAW,EACZ,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,eAAe,EACf,uBAAuB,EACvB,4BAA4B,EAC7B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAEL,8BAA8B,EAC/B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,MAAM,iBAAiB,CAAC;AAEpC,MAAM,CAAC,OAAO,OAAO,MAAM;IACzB,KAAK,EAAE,KAAK,CAAC;gBACD,KAAK,EAAE,KAAK;IAIxB,oBAAoB;IAIpB,uBAAuB,CAAC,IAAI,EAAE,MAAM;IAIpC,eAAe,CAAC,QAAQ,EAAE,MAAM;IAIhC,eAAe,CACb,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,2BAA2B,GAAG,MAAM;IAKlD;;;;;;OAMG;IACH,4BAA4B,CAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EAAE,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,WAAW,CAAC,yBAAyB,CAAC,CAAC;IASlD;;;OAGG;IACG,oBAAoB,IAAI,OAAO,CAAC,8BAA8B,EAAE,CAAC;CAGxE;AAED,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,eAAe,EACf,eAAe,EACf,4BAA4B,GAC7B,CAAC;AAEF;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,EACzC,KAAK,GACN,EAAE;IACD,KAAK,EAAE,KAAK,CAAC;CACd,GAAG,OAAO,CAAC,8BAA8B,EAAE,CAAC,CAG5C","file":"IdmOps.d.ts","sourcesContent":["import {\n IdObjectSkeletonInterface,\n NoIdObjectSkeletonInterface,\n PagedResult,\n} from '../api/ApiTypes';\nimport {\n getAllConfigEntities,\n getConfigEntity,\n putConfigEntity,\n getConfigEntitiesByType,\n queryAllManagedObjectsByType,\n} from '../api/IdmConfigApi';\nimport {\n testConnectorServers as _testConnectorServers,\n ConnectorServerStatusInterface,\n} from '../api/IdmSystemApi';\nimport State from '../shared/State';\n\nexport default class IdmOps {\n state: State;\n constructor(state: State) {\n this.state = state;\n }\n\n getAllConfigEntities() {\n return getAllConfigEntities({ state: this.state });\n }\n\n getConfigEntitiesByType(type: string) {\n return getConfigEntitiesByType({ type, state: this.state });\n }\n\n getConfigEntity(entityId: string) {\n return getConfigEntity({ entityId, state: this.state });\n }\n\n putConfigEntity(\n entityId: string,\n entityData: NoIdObjectSkeletonInterface | string\n ) {\n return putConfigEntity({ entityId, entityData, state: this.state });\n }\n\n /**\n * Query managed objects\n * @param {string} type managed object type\n * @param {string[]} fields fields to retrieve\n * @param {string} pageCookie paged results cookie\n * @returns {Promise<PagedResult<IdObjectSkeletonInterface>>} a promise that resolves to managed objects of the desired type\n */\n queryAllManagedObjectsByType(\n type: string,\n fields: string[],\n pageCookie: string\n ): Promise<PagedResult<IdObjectSkeletonInterface>> {\n return queryAllManagedObjectsByType({\n type,\n fields,\n pageCookie,\n state: this.state,\n });\n }\n\n /**\n * Test connector servers\n * @returns {Promise<ConnectorServerStatusInterface[]>} a promise that resolves to an array of ConnectorServerStatusInterface objects\n */\n async testConnectorServers(): Promise<ConnectorServerStatusInterface[]> {\n return testConnectorServers({ state: this.state });\n }\n}\n\nexport {\n getAllConfigEntities,\n getConfigEntitiesByType,\n getConfigEntity,\n putConfigEntity,\n queryAllManagedObjectsByType,\n};\n\n/**\n * Test connector servers\n * @returns {Promise<ConnectorServerStatusInterface[]>} a promise that resolves to an array of ConnectorServerStatusInterface objects\n */\nexport async function testConnectorServers({\n state,\n}: {\n state: State;\n}): Promise<ConnectorServerStatusInterface[]> {\n const response = await _testConnectorServers({ state });\n return response.openicf;\n}\n"]}
@@ -101,7 +101,7 @@ export default class JourneyOps {
101
101
  * Find all node configuration objects that are no longer referenced by any tree
102
102
  * @returns {Promise<unknown[]>} a promise that resolves to an array of orphaned nodes
103
103
  */
104
- findOrphanedNodes(): Promise<unknown[]>;
104
+ findOrphanedNodes(): Promise<NodeSkeleton[]>;
105
105
  /**
106
106
  * Remove orphaned nodes
107
107
  * @param {NodeSkeleton[]} orphanedNodes Pass in an array of orphaned node configuration objects to remove
@@ -279,7 +279,7 @@ export declare function getTreeDescendents({ treeExport, resolveTreeExport, reso
279
279
  */
280
280
  export declare function findOrphanedNodes({ state, }: {
281
281
  state: State;
282
- }): Promise<unknown[]>;
282
+ }): Promise<NodeSkeleton[]>;
283
283
  /**
284
284
  * Remove orphaned nodes
285
285
  * @param {NodeSkeleton[]} orphanedNodes Pass in an array of orphaned node configuration objects to remove
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ops/JourneyOps.ts"],"names":[],"mappings":"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","file":"JourneyOps.d.ts","sourcesContent":["import fs from 'fs';\nimport { v4 as uuidv4 } from 'uuid';\nimport _ from 'lodash';\nimport {\n convertBase64TextToArray,\n getTypedFilename,\n convertTextArrayToBase64,\n convertTextArrayToBase64Url,\n findFilesByName,\n getMetadata,\n} from './utils/ExportImportUtils';\nimport { getRealmManagedUser, replaceAll } from './utils/OpsUtils';\nimport State from '../shared/State';\nimport {\n getNode,\n putNode,\n deleteNode,\n getNodeTypes,\n getNodesByType,\n} from '../api/NodeApi';\nimport { isCloudOnlyNode, isCustomNode, isPremiumNode } from './NodeOps';\nimport { getTrees, getTree, putTree, deleteTree } from '../api/TreeApi';\nimport { getEmailTemplate, putEmailTemplate } from './EmailTemplateOps';\nimport { getScript } from '../api/ScriptApi';\nimport * as globalConfig from '../storage/StaticStorage';\nimport {\n printMessage,\n createProgressIndicator,\n updateProgressIndicator,\n stopProgressIndicator,\n debugMessage,\n} from './utils/Console';\nimport {\n getProviderByLocationAndId,\n getProviders,\n getProviderMetadata,\n createProvider,\n findProviders,\n updateProvider,\n} from '../api/Saml2Api';\nimport {\n createCircleOfTrust,\n getCirclesOfTrust,\n updateCircleOfTrust,\n} from '../api/CirclesOfTrustApi';\nimport {\n decode,\n encode,\n encodeBase64Url,\n isBase64Encoded,\n} from '../api/utils/Base64';\nimport {\n getSocialIdentityProviders,\n putProviderByTypeAndId,\n} from '../api/SocialIdentityProvidersApi';\nimport { getThemes, putThemes } from './ThemeOps';\nimport { putScript } from './ScriptOps';\nimport { JourneyClassification, TreeExportResolverInterface } from './OpsTypes';\nimport {\n InnerNodeRefSkeletonInterface,\n NodeRefSkeletonInterface,\n NodeSkeleton,\n ThemeSkeleton,\n TreeSkeleton,\n} from '../api/ApiTypes';\nimport {\n SingleTreeExportInterface,\n MultiTreeExportInterface,\n TreeDependencyMapInterface,\n TreeExportOptions,\n TreeImportOptions,\n} from './OpsTypes';\n\nexport default class JourneyOps {\n state: State;\n constructor(state: State) {\n this.state = state;\n }\n\n /**\n * Create an empty single tree export template\n * @returns {SingleTreeExportInterface} an empty single tree export template\n */\n createSingleTreeExportTemplate(): SingleTreeExportInterface {\n return createSingleTreeExportTemplate({ state: this.state });\n }\n\n /**\n * Create an empty multi tree export template\n * @returns {MultiTreeExportInterface} an empty multi tree export template\n */\n createMultiTreeExportTemplate(): MultiTreeExportInterface {\n return createMultiTreeExportTemplate({ state: this.state });\n }\n\n /**\n * Create export data for a tree/journey with all its nodes and dependencies. The export data can be written to a file as is.\n * @param {string} treeId tree id/name\n * @param {TreeExportOptions} options export options\n * @returns {Promise<SingleTreeExportInterface>} a promise that resolves to an object containing the tree and all its nodes and dependencies\n */\n async exportJourney(\n treeId: string,\n options: TreeExportOptions = {\n useStringArrays: true,\n deps: true,\n }\n ): Promise<SingleTreeExportInterface> {\n return exportJourney({ treeId, options, state: this.state });\n }\n\n /**\n * Get all the journeys/trees without all their nodes and dependencies.\n * @returns {Promise<TreeSkeleton[]>} a promise that resolves to an array of journey objects\n */\n async getJourneys(): Promise<TreeSkeleton[]> {\n return getJourneys({ state: this.state });\n }\n\n /**\n * Get a journey/tree without all its nodes and dependencies.\n * @param {string} journeyId journey id/name\n * @returns {Promise<TreeSkeleton>} a promise that resolves to a journey object\n */\n async getJourney(journeyId: string): Promise<TreeSkeleton> {\n return getJourney({ journeyId, state: this.state });\n }\n\n /**\n * Helper to import a tree with all dependencies from a `SingleTreeExportInterface` object (typically read from a file)\n * @param {SingleTreeExportInterface} treeObject tree object containing tree and all its dependencies\n * @param {TreeImportOptions} options import options\n * @returns {Promise<boolean>} a promise that resolves to true if no errors occurred during import\n */\n async importJourney(\n treeObject: SingleTreeExportInterface,\n options: TreeImportOptions\n ): Promise<boolean> {\n return importJourney({ treeObject, options, state: this.state });\n }\n\n /**\n * Resolve journey dependencies\n * @param {Map} installedJorneys Map of installed journeys\n * @param {Map} journeyMap Map of journeys to resolve dependencies for\n * @param {string[]} unresolvedJourneys Map to hold the names of unresolved journeys and their dependencies\n * @param {string[]} resolvedJourneys Array to hold the names of resolved journeys\n * @param {int} index Depth of recursion\n */\n async resolveDependencies(\n installedJorneys,\n journeyMap,\n unresolvedJourneys,\n resolvedJourneys,\n index = -1\n ) {\n return resolveDependencies(\n installedJorneys,\n journeyMap,\n unresolvedJourneys,\n resolvedJourneys,\n index\n );\n }\n\n /**\n * Helper to import multiple trees from a tree map\n * @param {MultiTreeExportInterface} treesMap map of trees object\n * @param {TreeImportOptions} options import options\n */\n async importAllJourneys(\n treesMap: MultiTreeExportInterface,\n options: TreeImportOptions\n ) {\n return importAllJourneys({ treesMap, options, state: this.state });\n }\n\n /**\n * Get the node reference obbject for a node object. Node reference objects\n * are used in a tree flow definition and within page nodes to reference\n * nodes. Among other things, node references contain all the non-configuration\n * meta data that exists for readaility, like the x/y coordinates of the node\n * and the display name chosen by the tree designer. The dislay name is the\n * only intuitive link between the graphical representation of the tree and\n * the node configurations that make up the tree.\n * @param nodeObj node object to retrieve the node reference object for\n * @param singleTreeExport tree export with or without dependencies\n * @returns {NodeRefSkeletonInterface | InnerNodeRefSkeletonInterface} node reference object\n */\n getNodeRef(\n nodeObj: NodeSkeleton,\n singleTreeExport: SingleTreeExportInterface\n ): NodeRefSkeletonInterface | InnerNodeRefSkeletonInterface {\n return getNodeRef(nodeObj, singleTreeExport);\n }\n\n /**\n * Default tree export resolver used to resolve a tree id/name to a full export\n * w/o dependencies of that tree from a platform instance.\n * @param {string} treeId id/name of the tree to resolve\n * @returns {TreeExportResolverInterface} tree export\n */\n onlineTreeExportResolver: TreeExportResolverInterface =\n onlineTreeExportResolver;\n\n /**\n * Tree export resolver used to resolve a tree id/name to a full export\n * of that tree from individual `treename.journey.json` export files.\n * @param {string} treeId id/name of the tree to resolve\n * @returns {TreeExportResolverInterface} tree export\n */\n fileByIdTreeExportResolver: TreeExportResolverInterface =\n fileByIdTreeExportResolver;\n\n /**\n * Factory that creates a tree export resolver used to resolve a tree id\n * to a full export of that tree from a multi-tree export file.\n * @param {string} file multi-tree export file\n * @returns {TreeExportResolverInterface} tree export resolver\n */\n createFileParamTreeExportResolver(file: string): TreeExportResolverInterface {\n return createFileParamTreeExportResolver(file, this.state);\n }\n\n /**\n * Get tree dependencies (all descendent inner trees)\n * @param {SingleTreeExportInterface} treeExport single tree export\n * @param {string[]} resolvedTreeIds list of tree ids wich have already been resolved\n * @param {TreeExportResolverInterface} resolveTreeExport tree export resolver callback function\n * @returns {Promise<TreeDependencyMapInterface>} a promise that resolves to a tree dependency map\n */\n async getTreeDescendents(\n treeExport: SingleTreeExportInterface,\n resolveTreeExport: TreeExportResolverInterface,\n resolvedTreeIds: string[] = []\n ): Promise<TreeDependencyMapInterface> {\n return getTreeDescendents({\n treeExport,\n resolveTreeExport,\n resolvedTreeIds,\n state: this.state,\n });\n }\n\n /**\n * Find all node configuration objects that are no longer referenced by any tree\n * @returns {Promise<unknown[]>} a promise that resolves to an array of orphaned nodes\n */\n async findOrphanedNodes(): Promise<unknown[]> {\n return findOrphanedNodes({ state: this.state });\n }\n\n /**\n * Remove orphaned nodes\n * @param {NodeSkeleton[]} orphanedNodes Pass in an array of orphaned node configuration objects to remove\n * @returns {Promise<NodeSkeleton[]>} a promise that resolves to an array nodes that encountered errors deleting\n */\n async removeOrphanedNodes(orphanedNodes: NodeSkeleton[]): Promise<unknown[]> {\n return removeOrphanedNodes({ orphanedNodes, state: this.state });\n }\n\n /**\n * Analyze if a journey contains any custom nodes considering the detected or the overridden version.\n * @param {SingleTreeExportInterface} journey Journey/tree configuration object\n * @returns {boolean} True if the journey/tree contains any custom nodes, false otherwise.\n */\n isCustomJourney(journey: SingleTreeExportInterface) {\n return isCustomJourney({ journey, state: this.state });\n }\n\n /**\n * Analyze if a journey contains any premium nodes considering the detected or the overridden version.\n * @param {SingleTreeExportInterface} journey Journey/tree configuration object\n * @returns {boolean} True if the journey/tree contains any custom nodes, false otherwise.\n */\n isPremiumJourney(journey: SingleTreeExportInterface) {\n return isPremiumJourney(journey);\n }\n\n /**\n * Analyze if a journey contains any cloud-only nodes considering the detected or the overridden version.\n * @param {SingleTreeExportInterface} journey Journey/tree configuration object\n * @returns {boolean} True if the journey/tree contains any cloud-only nodes, false otherwise.\n */\n isCloudOnlyJourney(journey: SingleTreeExportInterface) {\n return isCloudOnlyJourney(journey);\n }\n\n /**\n * Get a journey's classifications, which can be one or multiple of:\n * - standard: can run on any instance of a ForgeRock platform\n * - cloud: utilize nodes, which are exclusively available in the ForgeRock Identity Cloud\n * - premium: utilizes nodes, which come at a premium\n * - custom: utilizes nodes not included in the ForgeRock platform release\n * @param {SingleTreeExportInterface} journey journey export data\n * @returns {JourneyClassification[]} an array of one or multiple classifications\n */\n getJourneyClassification(\n journey: SingleTreeExportInterface\n ): JourneyClassification[] {\n return getJourneyClassification({ journey, state: this.state });\n }\n\n /**\n * Delete a journey\n * @param {string} journeyId journey id/name\n * @param {Object} options deep=true also delete all the nodes and inner nodes, verbose=true print verbose info\n */\n async deleteJourney(\n journeyId: string,\n options: { deep: boolean; verbose: boolean; progress?: boolean }\n ) {\n return deleteJourney({ journeyId, options, state: this.state });\n }\n\n /**\n * Delete all journeys\n * @param {Object} options deep=true also delete all the nodes and inner nodes, verbose=true print verbose info\n */\n async deleteJourneys(options: { deep: boolean; verbose: boolean }) {\n return deleteJourneys({ options, state: this.state });\n }\n\n /**\n * Enable a journey\n * @param journeyId journey id/name\n * @returns {Promise<boolean>} true if the operation was successful, false otherwise\n */\n async enableJourney(journeyId: string): Promise<boolean> {\n return enableJourney({ journeyId, state: this.state });\n }\n\n /**\n * Disable a journey\n * @param journeyId journey id/name\n * @returns {Promise<boolean>} true if the operation was successful, false otherwise\n */\n async disableJourney(journeyId: string): Promise<boolean> {\n return disableJourney({ journeyId, state: this.state });\n }\n}\n\nconst containerNodes = ['PageNode', 'CustomPageNode'];\n\nconst scriptedNodes = [\n 'ConfigProviderNode',\n 'ScriptedDecisionNode',\n 'ClientScriptNode',\n 'SocialProviderHandlerNode',\n 'CustomScriptNode',\n];\n\nconst emailTemplateNodes = ['EmailSuspendNode', 'EmailTemplateNode'];\n\nconst emptyScriptPlaceholder = '[Empty]';\n\n/**\n * Create an empty single tree export template\n * @returns {SingleTreeExportInterface} an empty single tree export template\n */\nfunction createSingleTreeExportTemplate({\n state,\n}: {\n state: State;\n}): SingleTreeExportInterface {\n return {\n meta: getMetadata({ state }),\n innerNodes: {},\n nodes: {},\n scripts: {},\n emailTemplates: {},\n socialIdentityProviders: {},\n themes: [],\n saml2Entities: {},\n circlesOfTrust: {},\n tree: {},\n } as SingleTreeExportInterface;\n}\n\n/**\n * Create an empty multi tree export template\n * @returns {MultiTreeExportInterface} an empty multi tree export template\n */\nfunction createMultiTreeExportTemplate({\n state,\n}: {\n state: State;\n}): MultiTreeExportInterface {\n return {\n meta: getMetadata({ state }),\n trees: {},\n } as MultiTreeExportInterface;\n}\n\n/**\n * Helper to get all SAML2 dependencies for a given node object\n * @param {Object} nodeObject node object\n * @param {[Object]} allProviders array of all saml2 providers objects\n * @param {[Object]} allCirclesOfTrust array of all circle of trust objects\n * @returns {Promise} a promise that resolves to an object containing a saml2 dependencies\n */\nasync function getSaml2NodeDependencies(\n nodeObject,\n allProviders,\n allCirclesOfTrust,\n state: State\n) {\n const samlProperties = ['metaAlias', 'idpEntityId'];\n const saml2EntityPromises = [];\n for (const samlProperty of samlProperties) {\n // In the following line nodeObject[samlProperty] will look like '/alpha/iSPAzure'.\n const entityId =\n samlProperty === 'metaAlias'\n ? _.last(nodeObject[samlProperty].split('/'))\n : nodeObject[samlProperty];\n const entity = _.find(allProviders, { entityId });\n if (entity) {\n try {\n const providerResponse = await getProviderByLocationAndId({\n location: entity.location,\n entityId64: entity._id,\n state,\n });\n /**\n * Adding entityLocation here to the entityResponse because the import tool\n * needs to know whether the saml2 entity is remote or not (this will be removed\n * from the config before importing see updateSaml2Entity and createSaml2Entity functions).\n * Importing a remote saml2 entity is a slightly different request (see createSaml2Entity).\n */\n providerResponse.entityLocation = entity.location;\n\n if (entity.location === 'remote') {\n // get the xml representation of this entity and add it to the entityResponse;\n const metaDataResponse = await getProviderMetadata({\n entityId: providerResponse.entityId,\n state,\n });\n providerResponse.base64EntityXML = encodeBase64Url(metaDataResponse);\n }\n saml2EntityPromises.push(providerResponse);\n } catch (error) {\n printMessage(error.message, 'error');\n }\n }\n }\n try {\n const saml2EntitiesPromisesResults = await Promise.all(saml2EntityPromises);\n const saml2Entities = [];\n for (const saml2Entity of saml2EntitiesPromisesResults) {\n if (saml2Entity) {\n saml2Entities.push(saml2Entity);\n }\n }\n const samlEntityIds = _.map(\n saml2Entities,\n (saml2EntityConfig) => `${saml2EntityConfig.entityId}|saml2`\n );\n const circlesOfTrust = _.filter(allCirclesOfTrust, (circleOfTrust) => {\n let hasEntityId = false;\n for (const trustedProvider of circleOfTrust.trustedProviders) {\n if (!hasEntityId && samlEntityIds.includes(trustedProvider)) {\n hasEntityId = true;\n }\n }\n return hasEntityId;\n });\n const saml2NodeDependencies = {\n saml2Entities,\n circlesOfTrust,\n };\n return saml2NodeDependencies;\n } catch (error) {\n printMessage(error.message, 'error');\n const saml2NodeDependencies = {\n saml2Entities: [],\n circlesOfTrust: [],\n };\n return saml2NodeDependencies;\n }\n}\n\n/**\n * Create export data for a tree/journey with all its nodes and dependencies. The export data can be written to a file as is.\n * @param {string} treeId tree id/name\n * @param {TreeExportOptions} options export options\n * @returns {Promise<SingleTreeExportInterface>} a promise that resolves to an object containing the tree and all its nodes and dependencies\n */\nexport async function exportJourney({\n treeId,\n options = {\n useStringArrays: true,\n deps: true,\n },\n state,\n}: {\n treeId: string;\n options?: TreeExportOptions;\n state: State;\n}): Promise<SingleTreeExportInterface> {\n const exportData = createSingleTreeExportTemplate({ state });\n try {\n const treeObject = await getTree({ id: treeId, state });\n const { useStringArrays, deps } = options;\n const verbose = state.getDebug();\n\n if (verbose) printMessage(`\\n- ${treeObject._id}\\n`, 'info', false);\n\n // Process tree\n if (verbose) printMessage(' - Flow');\n exportData.tree = treeObject;\n if (verbose && treeObject.identityResource)\n printMessage(\n ` - identityResource: ${treeObject.identityResource}`,\n 'info'\n );\n if (verbose) printMessage(` - Done`, 'info');\n\n const nodePromises = [];\n const scriptPromises = [];\n const emailTemplatePromises = [];\n const innerNodePromises = [];\n const saml2ConfigPromises = [];\n let socialProviderPromise = null;\n let themePromise = null;\n if (\n deps &&\n state.getDeploymentType() !== globalConfig.CLASSIC_DEPLOYMENT_TYPE_KEY\n ) {\n try {\n themePromise = getThemes({ state });\n } catch (error) {\n printMessage(error, 'error');\n }\n }\n\n let allSaml2Providers = null;\n let allCirclesOfTrust = null;\n let filteredSocialProviders = null;\n const themes = [];\n\n // get all the nodes\n for (const [nodeId, nodeInfo] of Object.entries(treeObject.nodes)) {\n nodePromises.push(\n getNode({ nodeId, nodeType: nodeInfo['nodeType'], state })\n );\n }\n if (verbose && nodePromises.length > 0) printMessage(' - Nodes:');\n const nodeObjects = await Promise.all(nodePromises);\n\n // iterate over every node in tree\n for (const nodeObject of nodeObjects) {\n const nodeId = nodeObject._id;\n const nodeType = nodeObject._type._id;\n if (verbose) printMessage(` - ${nodeId} (${nodeType})`, 'info', true);\n exportData.nodes[nodeObject._id] = nodeObject;\n\n // handle script node types\n if (\n deps &&\n scriptedNodes.includes(nodeType) &&\n nodeObject.script !== emptyScriptPlaceholder\n ) {\n scriptPromises.push(getScript({ scriptId: nodeObject.script, state }));\n }\n\n // frodo supports email templates in platform deployments\n if (\n (deps &&\n state.getDeploymentType() ===\n globalConfig.CLOUD_DEPLOYMENT_TYPE_KEY) ||\n state.getDeploymentType() === globalConfig.FORGEOPS_DEPLOYMENT_TYPE_KEY\n ) {\n if (emailTemplateNodes.includes(nodeType)) {\n try {\n const emailTemplate = await getEmailTemplate({\n templateId: nodeObject.emailTemplateName,\n state,\n });\n emailTemplatePromises.push(emailTemplate);\n } catch (error) {\n let message = `${error}`;\n if (error.isAxiosError && error.response.status) {\n message = error.response.statusText;\n }\n printMessage(\n `\\n${message}: Email Template \"${nodeObject.emailTemplateName}\"`,\n 'error'\n );\n }\n }\n }\n\n // handle SAML2 node dependencies\n if (deps && nodeType === 'product-Saml2Node') {\n if (!allSaml2Providers) {\n // eslint-disable-next-line no-await-in-loop\n allSaml2Providers = (await getProviders({ state })).result;\n }\n if (!allCirclesOfTrust) {\n // eslint-disable-next-line no-await-in-loop\n allCirclesOfTrust = (await getCirclesOfTrust({ state })).result;\n }\n saml2ConfigPromises.push(\n getSaml2NodeDependencies(\n nodeObject,\n allSaml2Providers,\n allCirclesOfTrust,\n state\n )\n );\n }\n\n // If this is a SocialProviderHandlerNode get each enabled social identity provider.\n if (\n deps &&\n !socialProviderPromise &&\n nodeType === 'SocialProviderHandlerNode'\n ) {\n socialProviderPromise = getSocialIdentityProviders({ state });\n }\n\n // If this is a SelectIdPNode and filteredProviters is not already set to empty array set filteredSocialProviers.\n if (deps && !filteredSocialProviders && nodeType === 'SelectIdPNode') {\n filteredSocialProviders = filteredSocialProviders || [];\n for (const filteredProvider of nodeObject.filteredProviders) {\n if (!filteredSocialProviders.includes(filteredProvider)) {\n filteredSocialProviders.push(filteredProvider);\n }\n }\n }\n\n // get inner nodes (nodes inside container nodes)\n if (containerNodes.includes(nodeType)) {\n for (const innerNode of nodeObject.nodes) {\n innerNodePromises.push(\n getNode({\n nodeId: innerNode._id,\n nodeType: innerNode.nodeType,\n state,\n })\n );\n }\n // frodo supports themes in platform deployments\n if (\n (deps &&\n state.getDeploymentType() ===\n globalConfig.CLOUD_DEPLOYMENT_TYPE_KEY) ||\n state.getDeploymentType() ===\n globalConfig.FORGEOPS_DEPLOYMENT_TYPE_KEY\n ) {\n let themeId = false;\n\n if (nodeObject.stage) {\n // see if themeId is part of the stage object\n try {\n themeId = JSON.parse(nodeObject.stage).themeId;\n } catch (e) {\n themeId = false;\n }\n // if the page node's themeId is set the \"old way\" set themeId accordingly\n if (!themeId && nodeObject.stage.indexOf('themeId=') === 0) {\n // eslint-disable-next-line prefer-destructuring\n themeId = nodeObject.stage.split('=')[1];\n }\n }\n\n if (themeId) {\n if (!themes.includes(themeId)) themes.push(themeId);\n }\n }\n }\n }\n\n // Process inner nodes\n if (verbose && innerNodePromises.length > 0)\n printMessage(' - Inner nodes:');\n const innerNodeDataResults = await Promise.all(innerNodePromises);\n for (const innerNodeObject of innerNodeDataResults) {\n const innerNodeId = innerNodeObject._id;\n const innerNodeType = innerNodeObject._type._id;\n if (verbose)\n printMessage(` - ${innerNodeId} (${innerNodeType})`, 'info', true);\n exportData.innerNodes[innerNodeId] = innerNodeObject;\n\n // handle script node types\n if (deps && scriptedNodes.includes(innerNodeType)) {\n scriptPromises.push(\n getScript({ scriptId: innerNodeObject.script, state })\n );\n }\n\n // frodo supports email templates in platform deployments\n if (\n (deps &&\n state.getDeploymentType() ===\n globalConfig.CLOUD_DEPLOYMENT_TYPE_KEY) ||\n state.getDeploymentType() === globalConfig.FORGEOPS_DEPLOYMENT_TYPE_KEY\n ) {\n if (emailTemplateNodes.includes(innerNodeType)) {\n try {\n const emailTemplate = await getEmailTemplate({\n templateId: innerNodeObject.emailTemplateName,\n state,\n });\n emailTemplatePromises.push(emailTemplate);\n } catch (error) {\n let message = `${error}`;\n if (error.isAxiosError && error.response.status) {\n message = error.response.statusText;\n }\n printMessage(\n `\\n${message}: Email Template \"${innerNodeObject.emailTemplateName}\"`,\n 'error'\n );\n }\n }\n }\n\n // handle SAML2 node dependencies\n if (deps && innerNodeType === 'product-Saml2Node') {\n printMessage('SAML2 inner node', 'error');\n if (!allSaml2Providers) {\n // eslint-disable-next-line no-await-in-loop\n allSaml2Providers = (await getProviders({ state })).result;\n }\n if (!allCirclesOfTrust) {\n // eslint-disable-next-line no-await-in-loop\n allCirclesOfTrust = (await getCirclesOfTrust({ state })).result;\n }\n saml2ConfigPromises.push(\n getSaml2NodeDependencies(\n innerNodeObject,\n allSaml2Providers,\n allCirclesOfTrust,\n state\n )\n );\n }\n\n // If this is a SocialProviderHandlerNode get each enabled social identity provider.\n if (\n deps &&\n !socialProviderPromise &&\n innerNodeType === 'SocialProviderHandlerNode'\n ) {\n socialProviderPromise = getSocialIdentityProviders({ state });\n }\n\n // If this is a SelectIdPNode and filteredProviters is not already set to empty array set filteredSocialProviers.\n if (\n deps &&\n !filteredSocialProviders &&\n innerNodeType === 'SelectIdPNode' &&\n innerNodeObject.filteredProviders\n ) {\n filteredSocialProviders = filteredSocialProviders || [];\n for (const filteredProvider of innerNodeObject.filteredProviders) {\n if (!filteredSocialProviders.includes(filteredProvider)) {\n filteredSocialProviders.push(filteredProvider);\n }\n }\n }\n }\n\n // Process email templates\n if (verbose && emailTemplatePromises.length > 0)\n printMessage(' - Email templates:');\n const settledEmailTemplatePromises = await Promise.allSettled(\n emailTemplatePromises\n );\n for (const settledPromise of settledEmailTemplatePromises) {\n if (settledPromise.status === 'fulfilled' && settledPromise.value) {\n if (verbose)\n printMessage(\n ` - ${settledPromise.value._id.split('/')[1]}${\n settledPromise.value.displayName\n ? ` (${settledPromise.value.displayName})`\n : ''\n }`,\n 'info',\n true\n );\n exportData.emailTemplates[settledPromise.value._id.split('/')[1]] =\n settledPromise.value;\n }\n }\n\n // Process SAML2 providers and circles of trust\n const saml2NodeDependencies = await Promise.all(saml2ConfigPromises);\n for (const saml2NodeDependency of saml2NodeDependencies) {\n if (saml2NodeDependency) {\n if (verbose) printMessage(' - SAML2 entity providers:');\n for (const saml2Entity of saml2NodeDependency.saml2Entities) {\n if (verbose)\n printMessage(\n ` - ${saml2Entity.entityLocation} ${saml2Entity.entityId}`,\n 'info'\n );\n exportData.saml2Entities[saml2Entity._id] = saml2Entity;\n }\n if (verbose) printMessage(' - SAML2 circles of trust:');\n for (const circleOfTrust of saml2NodeDependency.circlesOfTrust) {\n if (verbose) printMessage(` - ${circleOfTrust._id}`, 'info');\n exportData.circlesOfTrust[circleOfTrust._id] = circleOfTrust;\n }\n }\n }\n\n // Process socialIdentityProviders\n const socialProviders = await Promise.resolve(socialProviderPromise);\n if (socialProviders) {\n if (verbose) printMessage(' - OAuth2/OIDC (social) identity providers:');\n for (const socialProvider of socialProviders.result) {\n // If the list of socialIdentityProviders needs to be filtered based on the\n // filteredProviders property of a SelectIdPNode do it here.\n if (\n socialProvider &&\n (!filteredSocialProviders ||\n filteredSocialProviders.length === 0 ||\n filteredSocialProviders.includes(socialProvider._id))\n ) {\n if (verbose) printMessage(` - ${socialProvider._id}`, 'info');\n scriptPromises.push(\n getScript({ scriptId: socialProvider.transform, state })\n );\n exportData.socialIdentityProviders[socialProvider._id] =\n socialProvider;\n }\n }\n }\n\n // Process scripts\n if (verbose && scriptPromises.length > 0) printMessage(' - Scripts:');\n const scriptObjects = await Promise.all(scriptPromises);\n for (const scriptObject of scriptObjects) {\n if (scriptObject) {\n if (verbose)\n printMessage(\n ` - ${scriptObject._id} (${scriptObject.name})`,\n 'info',\n true\n );\n scriptObject.script = useStringArrays\n ? convertBase64TextToArray(scriptObject.script)\n : JSON.stringify(decode(scriptObject.script));\n exportData.scripts[scriptObject._id] = scriptObject;\n }\n }\n\n // Process themes\n if (themePromise) {\n if (verbose) printMessage(' - Themes:');\n try {\n const themePromiseResults = await Promise.resolve(themePromise);\n for (const themeObject of themePromiseResults) {\n if (\n themeObject &&\n // has the theme been specified by id or name in a page node?\n (themes.includes(themeObject._id) ||\n themes.includes(themeObject.name) ||\n // has this journey been linked to a theme?\n themeObject.linkedTrees?.includes(treeObject._id))\n ) {\n if (verbose)\n printMessage(\n ` - ${themeObject._id} (${themeObject.name})`,\n 'info'\n );\n exportData.themes.push(themeObject);\n }\n }\n } catch (error) {\n printMessage(error.response.data, 'error');\n printMessage('Error handling themes: ' + error.message, 'error');\n }\n }\n } catch (error) {\n printMessage(error.response.data, 'error');\n printMessage(\n 'Error exporting tree: ' + treeId + ' - ' + error.message,\n 'error'\n );\n }\n\n return exportData;\n}\n\n/**\n * Get all the journeys/trees without all their nodes and dependencies.\n * @returns {Promise<TreeSkeleton[]>} a promise that resolves to an array of journey objects\n */\nexport async function getJourneys({\n state,\n}: {\n state: State;\n}): Promise<TreeSkeleton[]> {\n const { result } = await getTrees({ state });\n result.sort((a, b) => a._id.localeCompare(b._id));\n return result;\n}\n\n/**\n * Get a journey/tree without all its nodes and dependencies.\n * @param {string} journeyId journey id/name\n * @returns {Promise<TreeSkeleton>} a promise that resolves to a journey object\n */\nexport async function getJourney({\n journeyId,\n state,\n}: {\n journeyId: string;\n state: State;\n}): Promise<TreeSkeleton> {\n const response = await getTree({ id: journeyId, state });\n return response;\n}\n\n/**\n * Helper to import a tree with all dependencies from a `SingleTreeExportInterface` object (typically read from a file)\n * @param {SingleTreeExportInterface} treeObject tree object containing tree and all its dependencies\n * @param {TreeImportOptions} options import options\n * @returns {Promise<boolean>} a promise that resolves to true if no errors occurred during import\n */\nexport async function importJourney({\n treeObject,\n options,\n state,\n}: {\n treeObject: SingleTreeExportInterface;\n options: TreeImportOptions;\n state: State;\n}): Promise<boolean> {\n const { reUuid, deps } = options;\n const verbose = state.getDebug();\n if (verbose) printMessage(`\\n- ${treeObject.tree._id}\\n`, 'info', false);\n let newUuid = '';\n const uuidMap = {};\n const treeId = treeObject.tree._id;\n\n // Process scripts\n if (\n deps &&\n treeObject.scripts &&\n Object.entries(treeObject.scripts).length > 0\n ) {\n if (verbose) printMessage(' - Scripts:');\n for (const [scriptId, scriptObject] of Object.entries(treeObject.scripts)) {\n if (verbose)\n printMessage(\n ` - ${scriptId} (${scriptObject['name']})`,\n 'info',\n false\n );\n // is the script stored as an array of strings or just b64 blob?\n if (Array.isArray(scriptObject['script'])) {\n scriptObject['script'] = convertTextArrayToBase64(\n scriptObject['script']\n );\n } else if (!isBase64Encoded(scriptObject['script'])) {\n scriptObject['script'] = encode(JSON.parse(scriptObject['script']));\n }\n try {\n await putScript({ scriptId, scriptData: scriptObject, state });\n } catch (error) {\n throw new Error(\n `Error importing script ${scriptObject['name']} (${scriptId}) in journey ${treeId}: ${error.message}`\n );\n }\n if (verbose) printMessage('');\n }\n }\n\n // Process email templates\n if (\n deps &&\n treeObject.emailTemplates &&\n Object.entries(treeObject.emailTemplates).length > 0\n ) {\n if (verbose) printMessage(' - Email templates:');\n for (const [templateId, templateData] of Object.entries(\n treeObject.emailTemplates\n )) {\n if (verbose) printMessage(` - ${templateId}`, 'info', false);\n try {\n await putEmailTemplate({ templateId, templateData, state });\n } catch (error) {\n printMessage(error.response.data, 'error');\n throw new Error(`Error importing email templates: ${error.message}`);\n }\n if (verbose) printMessage('');\n }\n }\n\n // Process themes\n if (deps && treeObject.themes && treeObject.themes.length > 0) {\n if (verbose) printMessage(' - Themes:');\n const themes: Map<string, ThemeSkeleton> = new Map<string, ThemeSkeleton>();\n for (const theme of treeObject.themes) {\n if (verbose)\n printMessage(` - ${theme['_id']} (${theme['name']})`, 'info');\n themes[theme['_id']] = theme;\n }\n try {\n await putThemes({ themeMap: themes, state });\n } catch (error) {\n throw new Error(`Error importing themes: ${error.message}`);\n }\n }\n\n // Process social providers\n if (\n deps &&\n treeObject.socialIdentityProviders &&\n Object.entries(treeObject.socialIdentityProviders).length > 0\n ) {\n if (verbose) printMessage(' - OAuth2/OIDC (social) identity providers:');\n for (const [providerId, providerData] of Object.entries(\n treeObject.socialIdentityProviders\n )) {\n if (verbose) printMessage(` - ${providerId}`, 'info');\n try {\n await putProviderByTypeAndId({\n type: providerData['_type']['_id'],\n id: providerId,\n providerData,\n state,\n });\n } catch (importError) {\n if (\n importError.response?.status === 500 &&\n importError.response?.data?.message ===\n 'Unable to update SMS config: Data validation failed for the attribute, Redirect after form post URL'\n ) {\n providerData['redirectAfterFormPostURI'] = '';\n try {\n await putProviderByTypeAndId({\n type: providerData['_type']['_id'],\n id: providerId,\n providerData,\n state,\n });\n } catch (importError2) {\n printMessage(importError.response?.data || importError, 'error');\n throw new Error(\n `Error importing provider ${providerId} in journey ${treeId}: ${importError}`\n );\n }\n } else {\n printMessage(importError.response?.data || importError, 'error');\n throw new Error(\n `\\nError importing provider ${providerId} in journey ${treeId}: ${importError}`\n );\n }\n }\n }\n }\n\n // Process saml providers\n if (\n deps &&\n treeObject.saml2Entities &&\n Object.entries(treeObject.saml2Entities).length > 0\n ) {\n if (verbose) printMessage(' - SAML2 entity providers:');\n for (const [, providerData] of Object.entries(treeObject.saml2Entities)) {\n delete providerData['_rev'];\n const entityId = providerData['entityId'];\n const entityLocation = providerData['entityLocation'];\n if (verbose) printMessage(` - ${entityLocation} ${entityId}`, 'info');\n let metaData = null;\n if (entityLocation === 'remote') {\n if (Array.isArray(providerData['base64EntityXML'])) {\n metaData = convertTextArrayToBase64Url(\n providerData['base64EntityXML']\n );\n } else {\n metaData = providerData['base64EntityXML'];\n }\n }\n delete providerData['entityLocation'];\n delete providerData['base64EntityXML'];\n // create the provider if it doesn't already exist, or just update it\n if (\n (\n await findProviders({\n filter: `entityId eq '${entityId}'`,\n fields: ['location'],\n state,\n })\n ).resultCount === 0\n ) {\n await createProvider({\n location: entityLocation,\n providerData,\n metaData,\n state,\n }).catch((createProviderErr) => {\n printMessage(\n createProviderErr.response?.data || createProviderErr,\n 'error'\n );\n throw new Error(`Error creating provider ${entityId}`);\n });\n } else {\n await updateProvider({\n location: entityLocation,\n providerData,\n state,\n }).catch((updateProviderErr) => {\n printMessage(\n updateProviderErr.response?.data || updateProviderErr,\n 'error'\n );\n throw new Error(`Error updating provider ${entityId}`);\n });\n }\n }\n }\n\n // Process circles of trust\n if (\n deps &&\n treeObject.circlesOfTrust &&\n Object.entries(treeObject.circlesOfTrust).length > 0\n ) {\n if (verbose) printMessage(' - SAML2 circles of trust:');\n for (const [cotId, cotData] of Object.entries(treeObject.circlesOfTrust)) {\n delete cotData['_rev'];\n if (verbose) printMessage(` - ${cotId}`, 'info');\n try {\n await createCircleOfTrust({ cotData, state });\n } catch (createCotErr) {\n if (\n createCotErr.response?.status === 409 ||\n createCotErr.response?.status === 500\n ) {\n try {\n await updateCircleOfTrust({ cotId, cotData, state });\n } catch (updateCotErr) {\n printMessage(createCotErr.response?.data || createCotErr, 'error');\n printMessage(updateCotErr.response?.data || updateCotErr, 'error');\n throw new Error(`Error creating/updating circle of trust ${cotId}`);\n }\n } else {\n printMessage(createCotErr.response?.data || createCotErr, 'error');\n throw new Error(`Error creating circle of trust ${cotId}`);\n }\n }\n }\n }\n\n // Process inner nodes\n let innerNodes = {};\n if (\n treeObject.innerNodes &&\n Object.entries(treeObject.innerNodes).length > 0\n ) {\n innerNodes = treeObject.innerNodes;\n }\n // old export file format\n else if (\n treeObject.innernodes &&\n Object.entries(treeObject.innernodes).length > 0\n ) {\n innerNodes = treeObject.innernodes;\n }\n if (Object.entries(innerNodes).length > 0) {\n if (verbose) printMessage(' - Inner nodes:', 'text', true);\n for (const [innerNodeId, innerNodeData] of Object.entries(innerNodes)) {\n delete innerNodeData['_rev'];\n const nodeType = innerNodeData['_type']['_id'];\n if (!reUuid) {\n newUuid = innerNodeId;\n } else {\n newUuid = uuidv4();\n uuidMap[innerNodeId] = newUuid;\n }\n innerNodeData['_id'] = newUuid;\n\n if (verbose)\n printMessage(\n ` - ${newUuid}${reUuid ? '*' : ''} (${nodeType})`,\n 'info',\n false\n );\n\n // If the node has an identityResource config setting\n // and the identityResource ends in 'user'\n // and the node's identityResource is the same as the tree's identityResource\n // change it to the current realm managed user identityResource otherwise leave it alone.\n if (\n innerNodeData['identityResource'] &&\n innerNodeData['identityResource'].endsWith('user') &&\n innerNodeData['identityResource'] === treeObject.tree.identityResource\n ) {\n innerNodeData['identityResource'] = `managed/${getRealmManagedUser({\n state,\n })}`;\n if (verbose)\n printMessage(\n `\\n - identityResource: ${innerNodeData['identityResource']}`,\n 'info',\n false\n );\n }\n try {\n await putNode({\n nodeId: newUuid,\n nodeType,\n nodeData: innerNodeData as NodeSkeleton,\n state,\n });\n } catch (nodeImportError) {\n if (\n nodeImportError.response.status === 400 &&\n nodeImportError.response.data.message ===\n 'Data validation failed for the attribute, Script'\n ) {\n throw new Error(\n `Missing script ${\n innerNodeData['script']\n } referenced by inner node ${innerNodeId}${\n innerNodeId === newUuid ? '' : ` [${newUuid}]`\n } (${innerNodeData['_type']['_id']}) in journey ${treeId}.`\n );\n } else if (\n nodeImportError.response?.status === 400 &&\n nodeImportError.response?.data?.message ===\n 'Invalid attribute specified.'\n ) {\n const { validAttributes } = nodeImportError.response.data.detail;\n validAttributes.push('_id');\n for (const attribute of Object.keys(innerNodeData)) {\n if (!validAttributes.includes(attribute)) {\n if (verbose)\n printMessage(\n `\\n - Removing invalid attribute: ${attribute}`,\n 'warn',\n false\n );\n delete innerNodeData[attribute];\n }\n }\n try {\n await putNode({\n nodeId: newUuid,\n nodeType,\n nodeData: innerNodeData as NodeSkeleton,\n state,\n });\n } catch (nodeImportError2) {\n printMessage(nodeImportError2.response.data, 'error');\n throw new Error(\n `Error importing node ${innerNodeId}${\n innerNodeId === newUuid ? '' : ` [${newUuid}]`\n } in journey ${treeId}`\n );\n }\n } else {\n printMessage(nodeImportError.response.data, 'error');\n throw new Error(\n `Error importing inner node ${innerNodeId}${\n innerNodeId === newUuid ? '' : ` [${newUuid}]`\n } in journey ${treeId}`\n );\n }\n }\n if (verbose) printMessage('');\n }\n }\n\n // Process nodes\n if (treeObject.nodes && Object.entries(treeObject.nodes).length > 0) {\n if (verbose) printMessage(' - Nodes:');\n // eslint-disable-next-line prefer-const\n for (let [nodeId, nodeData] of Object.entries(treeObject.nodes)) {\n delete nodeData['_rev'];\n const nodeType = nodeData['_type']['_id'];\n if (!reUuid) {\n newUuid = nodeId;\n } else {\n newUuid = uuidv4();\n uuidMap[nodeId] = newUuid;\n }\n nodeData['_id'] = newUuid;\n\n if (nodeType === 'PageNode' && reUuid) {\n for (const [, inPageNodeData] of Object.entries(nodeData['nodes'])) {\n const currentId = inPageNodeData['_id'];\n nodeData = JSON.parse(\n replaceAll(JSON.stringify(nodeData), currentId, uuidMap[currentId])\n );\n }\n }\n\n if (verbose)\n printMessage(\n ` - ${newUuid}${reUuid ? '*' : ''} (${nodeType})`,\n 'info',\n false\n );\n\n // If the node has an identityResource config setting\n // and the identityResource ends in 'user'\n // and the node's identityResource is the same as the tree's identityResource\n // change it to the current realm managed user identityResource otherwise leave it alone.\n if (\n nodeData.identityResource &&\n nodeData.identityResource.endsWith('user') &&\n nodeData.identityResource === treeObject.tree.identityResource\n ) {\n nodeData['identityResource'] = `managed/${getRealmManagedUser({\n state,\n })}`;\n if (verbose)\n printMessage(\n `\\n - identityResource: ${nodeData['identityResource']}`,\n 'info',\n false\n );\n }\n try {\n await putNode({ nodeId: newUuid, nodeType, nodeData, state });\n } catch (nodeImportError) {\n if (\n nodeImportError.response.status === 400 &&\n nodeImportError.response.data.message ===\n 'Data validation failed for the attribute, Script'\n ) {\n throw new Error(\n `Missing script ${nodeData['script']} referenced by node ${nodeId}${\n nodeId === newUuid ? '' : ` [${newUuid}]`\n } (${nodeData['_type']['_id']}) in journey ${treeId}.`\n );\n } else if (\n nodeImportError.response?.status === 400 &&\n nodeImportError.response?.data?.message ===\n 'Invalid attribute specified.'\n ) {\n const { validAttributes } = nodeImportError.response.data.detail;\n validAttributes.push('_id');\n for (const attribute of Object.keys(nodeData)) {\n if (!validAttributes.includes(attribute)) {\n if (verbose)\n printMessage(\n `\\n - Removing invalid attribute: ${attribute}`,\n 'warn',\n false\n );\n delete nodeData[attribute];\n }\n }\n try {\n await putNode({ nodeId: newUuid, nodeType, nodeData, state });\n } catch (nodeImportError2) {\n printMessage(nodeImportError2.response.data, 'error');\n throw new Error(\n `Error importing node ${nodeId}${\n nodeId === newUuid ? '' : ` [${newUuid}]`\n } in journey ${treeId}`\n );\n }\n } else {\n printMessage(nodeImportError.response.data, 'error');\n throw new Error(\n `Error importing node ${nodeId}${\n nodeId === newUuid ? '' : ` [${newUuid}]`\n } in journey ${treeId}`\n );\n }\n }\n if (verbose) printMessage('');\n }\n }\n\n // Process tree\n if (verbose) printMessage(' - Flow');\n\n if (reUuid) {\n let journeyText = JSON.stringify(treeObject.tree, null, 2);\n for (const [oldId, newId] of Object.entries(uuidMap)) {\n journeyText = replaceAll(journeyText, oldId, newId);\n }\n treeObject.tree = JSON.parse(journeyText);\n }\n\n // If the tree has an identityResource config setting\n // and the identityResource ends in 'user'\n // Set the identityResource for the tree to the selected resource.\n if (\n (treeObject.tree.identityResource &&\n (treeObject.tree['identityResource'] as string).endsWith('user')) ||\n state.getDeploymentType() === globalConfig.CLOUD_DEPLOYMENT_TYPE_KEY ||\n state.getDeploymentType() === globalConfig.FORGEOPS_DEPLOYMENT_TYPE_KEY\n ) {\n treeObject.tree.identityResource = `managed/${getRealmManagedUser({\n state,\n })}`;\n if (verbose)\n printMessage(\n ` - identityResource: ${treeObject.tree.identityResource}`,\n 'info',\n false\n );\n }\n\n delete treeObject.tree._rev;\n try {\n await putTree({\n treeId: treeObject.tree._id as string,\n treeData: treeObject.tree,\n state,\n });\n if (verbose) printMessage(`\\n - Done`, 'info', true);\n } catch (importError) {\n if (\n importError.response?.status === 400 &&\n importError.response?.data?.message === 'Invalid attribute specified.'\n ) {\n const { validAttributes } = importError.response.data.detail;\n validAttributes.push('_id');\n for (const attribute of Object.keys(treeObject.tree)) {\n if (!validAttributes.includes(attribute)) {\n if (verbose)\n printMessage(\n `\\n - Removing invalid attribute: ${attribute}`,\n 'warn',\n false\n );\n delete treeObject.tree[attribute];\n }\n }\n try {\n await putTree({\n treeId: treeObject.tree._id as string,\n treeData: treeObject.tree,\n state,\n });\n if (verbose) printMessage(`\\n - Done`, 'info', true);\n } catch (importError2) {\n printMessage(importError2.response.data, 'error');\n throw new Error(`Error importing journey flow ${treeId}`);\n }\n } else {\n printMessage(importError.response?.data || importError, 'error');\n debugMessage(importError.response?.data || importError);\n throw new Error(`\\nError importing journey flow ${treeId}`);\n }\n }\n return true;\n}\n\n/**\n * Resolve journey dependencies\n * @param {Map} installedJorneys Map of installed journeys\n * @param {Map} journeyMap Map of journeys to resolve dependencies for\n * @param {[String]} unresolvedJourneys Map to hold the names of unresolved journeys and their dependencies\n * @param {[String]} resolvedJourneys Array to hold the names of resolved journeys\n * @param {int} index Depth of recursion\n */\nexport async function resolveDependencies(\n installedJorneys,\n journeyMap,\n unresolvedJourneys,\n resolvedJourneys,\n index = -1\n) {\n let before = -1;\n let after = index;\n if (index !== -1) {\n before = index;\n }\n\n for (const tree in journeyMap) {\n if ({}.hasOwnProperty.call(journeyMap, tree)) {\n const dependencies = [];\n for (const node in journeyMap[tree].nodes) {\n if (\n journeyMap[tree].nodes[node]._type._id === 'InnerTreeEvaluatorNode'\n ) {\n dependencies.push(journeyMap[tree].nodes[node].tree);\n }\n }\n let allResolved = true;\n for (const dependency of dependencies) {\n if (\n !resolvedJourneys.includes(dependency) &&\n !installedJorneys.includes(dependency)\n ) {\n allResolved = false;\n }\n }\n if (allResolved) {\n if (resolvedJourneys.indexOf(tree) === -1) resolvedJourneys.push(tree);\n // remove from unresolvedJourneys array\n // for (let i = 0; i < unresolvedJourneys.length; i += 1) {\n // if (unresolvedJourneys[i] === tree) {\n // unresolvedJourneys.splice(i, 1);\n // i -= 1;\n // }\n // }\n delete unresolvedJourneys[tree];\n // } else if (!unresolvedJourneys.includes(tree)) {\n } else {\n // unresolvedJourneys.push(tree);\n unresolvedJourneys[tree] = dependencies;\n }\n }\n }\n after = Object.keys(unresolvedJourneys).length;\n if (index !== -1 && after === before) {\n // This is the end, no progress was made since the last recursion\n // printMessage(\n // `Journeys with unresolved dependencies: ${unresolvedJourneys}`,\n // 'error'\n // );\n } else if (after > 0) {\n resolveDependencies(\n installedJorneys,\n journeyMap,\n unresolvedJourneys,\n resolvedJourneys,\n after\n );\n }\n}\n\n/**\n * Helper to import multiple trees from a tree map\n * @param {Object} treesMap map of trees object\n * @param {TreeImportOptions} options import options\n */\nexport async function importAllJourneys({\n treesMap,\n options,\n state,\n}: {\n treesMap: MultiTreeExportInterface;\n options: TreeImportOptions;\n state: State;\n}) {\n const installedJourneys = (await getTrees({ state })).result.map(\n (x) => x._id\n );\n const unresolvedJourneys = {};\n const resolvedJourneys = [];\n createProgressIndicator(undefined, 'Resolving dependencies', 'indeterminate');\n await resolveDependencies(\n installedJourneys,\n treesMap,\n unresolvedJourneys,\n resolvedJourneys\n );\n if (Object.keys(unresolvedJourneys).length === 0) {\n stopProgressIndicator(`Resolved all dependencies.`, 'success');\n } else {\n stopProgressIndicator(\n `${\n Object.keys(unresolvedJourneys).length\n } journeys with unresolved dependencies:`,\n 'fail'\n );\n for (const journey of Object.keys(unresolvedJourneys)) {\n printMessage(\n ` - ${journey} requires ${unresolvedJourneys[journey]}`,\n 'info'\n );\n }\n }\n createProgressIndicator(resolvedJourneys.length, 'Importing');\n for (const tree of resolvedJourneys) {\n try {\n // eslint-disable-next-line no-await-in-loop\n await importJourney({ treeObject: treesMap[tree], options, state });\n updateProgressIndicator(`${tree}`);\n } catch (error) {\n printMessage(`\\n${error.message}`, 'error');\n }\n }\n stopProgressIndicator('Done');\n}\n\n/**\n * Get the node reference obbject for a node object. Node reference objects\n * are used in a tree flow definition and within page nodes to reference\n * nodes. Among other things, node references contain all the non-configuration\n * meta data that exists for readaility, like the x/y coordinates of the node\n * and the display name chosen by the tree designer. The dislay name is the\n * only intuitive link between the graphical representation of the tree and\n * the node configurations that make up the tree.\n * @param nodeObj node object to retrieve the node reference object for\n * @param singleTreeExport tree export with or without dependencies\n * @returns {NodeRefSkeletonInterface | InnerNodeRefSkeletonInterface} node reference object\n */\nexport function getNodeRef(\n nodeObj: NodeSkeleton,\n singleTreeExport: SingleTreeExportInterface\n): NodeRefSkeletonInterface | InnerNodeRefSkeletonInterface {\n if (singleTreeExport.tree.nodes[nodeObj._id]) {\n return singleTreeExport.tree.nodes[nodeObj._id];\n } else {\n for (const node of Object.values(singleTreeExport.nodes)) {\n if (containerNodes.includes(node._type._id)) {\n for (const nodeRef of node.nodes) {\n if (nodeRef._id === nodeObj._id) {\n return nodeRef;\n }\n }\n }\n }\n }\n return undefined;\n}\n\n/**\n * Default tree export resolver used to resolve a tree id/name to a full export\n * w/o dependencies of that tree from a platform instance.\n * @param {string} treeId id/name of the tree to resolve\n * @returns {TreeExportResolverInterface} tree export\n */\nexport const onlineTreeExportResolver: TreeExportResolverInterface =\n async function (treeId: string, state: State) {\n debugMessage(`onlineTreeExportResolver(${treeId})`);\n return await exportJourney({\n treeId,\n options: {\n deps: false,\n useStringArrays: false,\n },\n state,\n });\n };\n\n/**\n * Tree export resolver used to resolve a tree id/name to a full export\n * of that tree from individual `treename.journey.json` export files.\n * @param {string} treeId id/name of the tree to resolve\n * @returns {TreeExportResolverInterface} tree export\n */\nexport const fileByIdTreeExportResolver: TreeExportResolverInterface =\n async function (treeId: string, state: State) {\n debugMessage(`fileByIdTreeExportResolver(${treeId})`);\n let treeExport = createSingleTreeExportTemplate({ state });\n const files = findFilesByName(getTypedFilename(`${treeId}`, 'journey'));\n try {\n const file = files.pop();\n const jsonData = JSON.parse(fs.readFileSync(file, 'utf8'));\n debugMessage(\n `fileByIdTreeExportResolver: resolved '${treeId}' to ${file}`\n );\n // did we resolve the tree we were asked to resolved?\n if (jsonData.tree?._id === treeId) {\n treeExport = jsonData;\n }\n // check if this is a file with multiple trees and get journey by id\n else if (jsonData.trees && jsonData.trees[treeId]) {\n treeExport = jsonData.trees[treeId];\n }\n } catch (error) {\n debugMessage(\n `fileByIdTreeExportResolver: unable to resolve '${treeId}' to a file.`\n );\n }\n return treeExport;\n };\n\n/**\n * Factory that creates a tree export resolver used to resolve a tree id\n * to a full export of that tree from a multi-tree export file.\n * @param {string} file multi-tree export file\n * @returns {TreeExportResolverInterface} tree export resolver\n */\nexport function createFileParamTreeExportResolver(\n file: string,\n state: State\n): TreeExportResolverInterface {\n const fileParamTreeExportResolver: TreeExportResolverInterface =\n async function (treeId: string) {\n debugMessage(`fileParamTreeExportResolver(${treeId})`);\n let treeExport: SingleTreeExportInterface =\n createSingleTreeExportTemplate({ state });\n try {\n const jsonData = JSON.parse(fs.readFileSync(file, 'utf8'));\n // did we resolve the tree we were asked to resolved?\n if (jsonData.tree?._id === treeId) {\n treeExport = jsonData;\n }\n // check if this is a file with multiple trees and get journey by id\n else if (jsonData.trees && jsonData.trees[treeId]) {\n treeExport = jsonData.trees[treeId];\n }\n // fall back to fileByIdTreeExportResolver\n else {\n treeExport = await fileByIdTreeExportResolver(treeId, state);\n }\n } catch (error) {\n //\n }\n return treeExport;\n };\n debugMessage(`fileParamTreeExportResolver: file=${file}`);\n return fileParamTreeExportResolver;\n}\n\n/**\n * Get tree dependencies (all descendent inner trees)\n * @param {SingleTreeExportInterface} treeExport single tree export\n * @param {string[]} resolvedTreeIds list of tree ids wich have already been resolved\n * @param {TreeExportResolverInterface} resolveTreeExport tree export resolver callback function\n * @returns {Promise<TreeDependencyMapInterface>} a promise that resolves to a tree dependency map\n */\nexport async function getTreeDescendents({\n treeExport,\n resolveTreeExport = onlineTreeExportResolver,\n resolvedTreeIds = [],\n state,\n}: {\n treeExport: SingleTreeExportInterface;\n resolveTreeExport: TreeExportResolverInterface;\n resolvedTreeIds: string[];\n state: State;\n}): Promise<TreeDependencyMapInterface> {\n debugMessage(\n `getTreeDependencies(${treeExport.tree._id}, [${resolvedTreeIds.join(\n ', '\n )}])`\n );\n if (!resolvedTreeIds.includes(treeExport.tree._id)) {\n resolvedTreeIds.push(treeExport.tree._id);\n }\n const treeDependencyMap: TreeDependencyMapInterface = {\n [treeExport.tree._id]: [],\n };\n const dependencies: TreeDependencyMapInterface[] = [];\n for (const [nodeId, node] of Object.entries(treeExport.tree.nodes)) {\n const innerTreeId = treeExport.nodes[nodeId].tree;\n if (\n node.nodeType === 'InnerTreeEvaluatorNode' &&\n !resolvedTreeIds.includes(innerTreeId)\n ) {\n const innerTreeExport = await resolveTreeExport(innerTreeId, state);\n debugMessage(`resolved inner tree: ${innerTreeExport.tree._id}`);\n // resolvedTreeIds.push(innerTreeId);\n dependencies.push(\n await getTreeDescendents({\n treeExport: innerTreeExport,\n resolveTreeExport,\n resolvedTreeIds,\n state,\n })\n );\n }\n }\n treeDependencyMap[treeExport.tree._id] = dependencies;\n return treeDependencyMap;\n}\n\n/**\n * Find all node configuration objects that are no longer referenced by any tree\n * @returns {Promise<unknown[]>} a promise that resolves to an array of orphaned nodes\n */\nexport async function findOrphanedNodes({\n state,\n}: {\n state: State;\n}): Promise<unknown[]> {\n const allNodes = [];\n const orphanedNodes = [];\n let types = [];\n const allJourneys = (await getTrees({ state })).result;\n let errorMessage = '';\n const errorTypes = [];\n\n createProgressIndicator(\n undefined,\n `Counting total nodes...`,\n 'indeterminate'\n );\n try {\n types = (await getNodeTypes({ state })).result;\n } catch (error) {\n printMessage('Error retrieving all available node types:', 'error');\n printMessage(error.response.data, 'error');\n return [];\n }\n for (const type of types) {\n try {\n // eslint-disable-next-line no-await-in-loop, no-loop-func\n const nodes = (await getNodesByType({ nodeType: type._id, state }))\n .result;\n for (const node of nodes) {\n allNodes.push(node);\n updateProgressIndicator(\n `${allNodes.length} total nodes${errorMessage}`\n );\n }\n } catch (error) {\n errorTypes.push(type._id);\n errorMessage = ` (Skipped type(s): ${errorTypes})`['yellow'];\n updateProgressIndicator(`${allNodes.length} total nodes${errorMessage}`);\n }\n }\n if (errorTypes.length > 0) {\n stopProgressIndicator(\n `${allNodes.length} total nodes${errorMessage}`,\n 'warn'\n );\n } else {\n stopProgressIndicator(`${allNodes.length} total nodes`, 'success');\n }\n\n createProgressIndicator(\n undefined,\n 'Counting active nodes...',\n 'indeterminate'\n );\n const activeNodes = [];\n for (const journey of allJourneys) {\n for (const nodeId in journey.nodes) {\n if ({}.hasOwnProperty.call(journey.nodes, nodeId)) {\n activeNodes.push(nodeId);\n updateProgressIndicator(`${activeNodes.length} active nodes`);\n const node = journey.nodes[nodeId];\n if (containerNodes.includes(node.nodeType)) {\n const containerNode = await getNode({\n nodeId,\n nodeType: node.nodeType,\n state,\n });\n for (const innerNode of containerNode.nodes) {\n activeNodes.push(innerNode._id);\n updateProgressIndicator(`${activeNodes.length} active nodes`);\n }\n }\n }\n }\n }\n stopProgressIndicator(`${activeNodes.length} active nodes`, 'success');\n\n createProgressIndicator(\n undefined,\n 'Calculating orphaned nodes...',\n 'indeterminate'\n );\n const diff = allNodes.filter((x) => !activeNodes.includes(x._id));\n for (const orphanedNode of diff) {\n orphanedNodes.push(orphanedNode);\n }\n stopProgressIndicator(`${orphanedNodes.length} orphaned nodes`, 'success');\n return orphanedNodes;\n}\n\n/**\n * Remove orphaned nodes\n * @param {NodeSkeleton[]} orphanedNodes Pass in an array of orphaned node configuration objects to remove\n * @returns {Promise<NodeSkeleton[]>} a promise that resolves to an array nodes that encountered errors deleting\n */\nexport async function removeOrphanedNodes({\n orphanedNodes,\n state,\n}: {\n orphanedNodes: NodeSkeleton[];\n state: State;\n}): Promise<NodeSkeleton[]> {\n const errorNodes = [];\n createProgressIndicator(orphanedNodes.length, 'Removing orphaned nodes...');\n for (const node of orphanedNodes) {\n updateProgressIndicator(`Removing ${node['_id']}...`);\n try {\n // eslint-disable-next-line no-await-in-loop\n await deleteNode({\n nodeId: node['_id'],\n nodeType: node['_type']['_id'],\n state,\n });\n } catch (deleteError) {\n errorNodes.push(node);\n printMessage(` ${deleteError}`, 'error');\n }\n }\n stopProgressIndicator(`Removed ${orphanedNodes.length} orphaned nodes.`);\n return errorNodes;\n}\n\n/**\n * Analyze if a journey contains any custom nodes considering the detected or the overridden version.\n * @param {SingleTreeExportInterface} journey Journey/tree configuration object\n * @returns {boolean} True if the journey/tree contains any custom nodes, false otherwise.\n */\nexport function isCustomJourney({\n journey,\n state,\n}: {\n journey: SingleTreeExportInterface;\n state: State;\n}) {\n const nodeList = Object.values(journey.nodes).concat(\n Object.values(journey.innerNodes)\n );\n for (const node of nodeList) {\n if (isCustomNode({ nodeType: node['_type']['_id'], state })) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Analyze if a journey contains any premium nodes considering the detected or the overridden version.\n * @param {SingleTreeExportInterface} journey Journey/tree configuration object\n * @returns {boolean} True if the journey/tree contains any custom nodes, false otherwise.\n */\nexport function isPremiumJourney(journey: SingleTreeExportInterface) {\n const nodeList = Object.values(journey.nodes).concat(\n Object.values(journey.innerNodes)\n );\n for (const node of nodeList) {\n if (isPremiumNode(node['_type']['_id'])) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Analyze if a journey contains any cloud-only nodes considering the detected or the overridden version.\n * @param {SingleTreeExportInterface} journey Journey/tree configuration object\n * @returns {boolean} True if the journey/tree contains any cloud-only nodes, false otherwise.\n */\nexport function isCloudOnlyJourney(journey: SingleTreeExportInterface) {\n const nodeList = Object.values(journey.nodes).concat(\n Object.values(journey.innerNodes)\n );\n for (const node of nodeList) {\n if (isCloudOnlyNode(node['_type']['_id'])) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Get a journey's classifications, which can be one or multiple of:\n * - standard: can run on any instance of a ForgeRock platform\n * - cloud: utilize nodes, which are exclusively available in the ForgeRock Identity Cloud\n * - premium: utilizes nodes, which come at a premium\n * - custom: utilizes nodes not included in the ForgeRock platform release\n * @param {SingleTreeExportInterface} journey journey export data\n * @returns {JourneyClassification[]} an array of one or multiple classifications\n */\nexport function getJourneyClassification({\n journey,\n state,\n}: {\n journey: SingleTreeExportInterface;\n state: State;\n}): JourneyClassification[] {\n const classifications: JourneyClassification[] = [];\n const premium = isPremiumJourney(journey);\n const custom = isCustomJourney({ journey, state });\n const cloud = isCloudOnlyJourney(journey);\n if (custom) {\n classifications.push(JourneyClassification.CUSTOM);\n } else if (cloud) {\n classifications.push(JourneyClassification.CLOUD);\n } else {\n classifications.push(JourneyClassification.STANDARD);\n }\n if (premium) classifications.push(JourneyClassification.PREMIUM);\n return classifications;\n}\n\n/**\n * Delete a journey\n * @param {string} journeyId journey id/name\n * @param {Object} options deep=true also delete all the nodes and inner nodes, verbose=true print verbose info\n */\nexport async function deleteJourney({\n journeyId,\n options,\n state,\n}: {\n journeyId: string;\n options: { deep: boolean; verbose: boolean; progress?: boolean };\n state: State;\n}) {\n const { deep, verbose } = options;\n const progress = !('progress' in options) ? true : options.progress;\n const status = { nodes: {} };\n if (progress)\n createProgressIndicator(\n undefined,\n `Deleting ${journeyId}...`,\n 'indeterminate'\n );\n if (progress && verbose) stopProgressIndicator();\n return deleteTree({ treeId: journeyId, state })\n .then(async (deleteTreeResponse) => {\n status['status'] = 'success';\n const nodePromises = [];\n if (verbose) printMessage(`Deleted ${journeyId} (tree)`, 'info');\n if (deep) {\n for (const [nodeId, nodeObject] of Object.entries(\n deleteTreeResponse.nodes\n )) {\n // delete inner nodes (nodes inside container nodes)\n if (containerNodes.includes(nodeObject['nodeType'])) {\n try {\n // eslint-disable-next-line no-await-in-loop\n const containerNode = await getNode({\n nodeId,\n nodeType: nodeObject['nodeType'],\n state,\n });\n if (verbose)\n printMessage(\n `Read ${nodeId} (${nodeObject['nodeType']}) from ${journeyId}`,\n 'info'\n );\n for (const innerNodeObject of containerNode.nodes) {\n nodePromises.push(\n deleteNode({\n nodeId: innerNodeObject._id,\n nodeType: innerNodeObject.nodeType,\n state,\n })\n .then((response2) => {\n status.nodes[innerNodeObject._id] = { status: 'success' };\n if (verbose)\n printMessage(\n `Deleted ${innerNodeObject._id} (${innerNodeObject.nodeType}) from ${journeyId}`,\n 'info'\n );\n return response2;\n })\n .catch((error) => {\n status.nodes[innerNodeObject._id] = {\n status: 'error',\n error,\n };\n if (verbose)\n printMessage(\n `Error deleting inner node ${innerNodeObject._id} (${innerNodeObject.nodeType}) from ${journeyId}: ${error}`,\n 'error'\n );\n })\n );\n }\n // finally delete the container node\n nodePromises.push(\n deleteNode({\n nodeId: containerNode._id,\n nodeType: containerNode['_type']['_id'],\n state,\n })\n .then((response2) => {\n status.nodes[containerNode._id] = { status: 'success' };\n if (verbose)\n printMessage(\n `Deleted ${containerNode._id} (${containerNode['_type']['_id']}) from ${journeyId}`,\n 'info'\n );\n return response2;\n })\n .catch((error) => {\n if (\n error?.response?.data?.code === 500 &&\n error.response.data.message ===\n 'Unable to read SMS config: Node did not exist'\n ) {\n status.nodes[containerNode._id] = { status: 'success' };\n if (verbose)\n printMessage(\n `Deleted ${containerNode._id} (${containerNode['_type']['_id']}) from ${journeyId}`,\n 'info'\n );\n } else {\n status.nodes[containerNode._id] = {\n status: 'error',\n error,\n };\n if (verbose)\n printMessage(\n `Error deleting container node ${containerNode._id} (${containerNode['_type']['_id']}) from ${journeyId}: ${error.response.data.message}`,\n 'error'\n );\n }\n })\n );\n } catch (error) {\n if (verbose)\n printMessage(\n `Error getting container node ${nodeId} (${nodeObject['nodeType']}) from ${journeyId}: ${error}`,\n 'error'\n );\n }\n } else {\n // delete the node\n nodePromises.push(\n deleteNode({ nodeId, nodeType: nodeObject['nodeType'], state })\n .then((response) => {\n status.nodes[nodeId] = { status: 'success' };\n if (verbose)\n printMessage(\n `Deleted ${nodeId} (${nodeObject['nodeType']}) from ${journeyId}`,\n 'info'\n );\n return response;\n })\n .catch((error) => {\n status.nodes[nodeId] = { status: 'error', error };\n if (verbose)\n printMessage(\n `Error deleting node ${nodeId} (${nodeObject['nodeType']}) from ${journeyId}: ${error}`,\n 'error'\n );\n })\n );\n }\n }\n }\n // wait until all the node calls are complete\n await Promise.allSettled(nodePromises);\n\n // report status\n if (progress) {\n let nodeCount = 0;\n let errorCount = 0;\n for (const node of Object.keys(status.nodes)) {\n nodeCount += 1;\n if (status.nodes[node].status === 'error') errorCount += 1;\n }\n if (errorCount === 0) {\n stopProgressIndicator(\n `Deleted ${journeyId} and ${\n nodeCount - errorCount\n }/${nodeCount} nodes.`,\n 'success'\n );\n } else {\n stopProgressIndicator(\n `Deleted ${journeyId} and ${\n nodeCount - errorCount\n }/${nodeCount} nodes.`,\n 'fail'\n );\n }\n }\n return status;\n })\n .catch((error) => {\n status['status'] = 'error';\n status['error'] = error;\n stopProgressIndicator(`Error deleting ${journeyId}.`, 'fail');\n if (verbose)\n printMessage(`Error deleting tree ${journeyId}: ${error}`, 'error');\n return status;\n });\n}\n\n/**\n * Delete all journeys\n * @param {Object} options deep=true also delete all the nodes and inner nodes, verbose=true print verbose info\n */\nexport async function deleteJourneys({\n options,\n state,\n}: {\n options?: {\n deep: boolean;\n verbose: boolean;\n };\n state: State;\n}) {\n const { verbose } = options;\n const status = {};\n const trees = (await getTrees({ state })).result;\n createProgressIndicator(trees.length, 'Deleting journeys...');\n for (const tree of trees) {\n if (verbose) printMessage('');\n options['progress'] = false;\n status[tree._id] = await deleteJourney({\n journeyId: tree._id,\n options,\n state,\n });\n updateProgressIndicator(`${tree._id}`);\n // introduce a 100ms wait to allow the progress bar to update before the next verbose message prints from the async function\n if (verbose)\n // eslint-disable-next-line no-await-in-loop\n await new Promise((r) => {\n setTimeout(r, 100);\n });\n }\n let journeyCount = 0;\n let journeyErrorCount = 0;\n let nodeCount = 0;\n let nodeErrorCount = 0;\n for (const journey of Object.keys(status)) {\n journeyCount += 1;\n if (status[journey].status === 'error') journeyErrorCount += 1;\n for (const node of Object.keys(status[journey].nodes)) {\n nodeCount += 1;\n if (status[journey].nodes[node].status === 'error') nodeErrorCount += 1;\n }\n }\n stopProgressIndicator(\n `Deleted ${journeyCount - journeyErrorCount}/${journeyCount} journeys and ${\n nodeCount - nodeErrorCount\n }/${nodeCount} nodes.`\n );\n return status;\n}\n\n/**\n * Enable a journey\n * @param journeyId journey id/name\n * @returns {Promise<boolean>} true if the operation was successful, false otherwise\n */\nexport async function enableJourney({\n journeyId,\n state,\n}: {\n journeyId: string;\n state: State;\n}): Promise<boolean> {\n try {\n const treeObject = await getTree({ id: journeyId, state });\n treeObject['enabled'] = true;\n delete treeObject._rev;\n const newTreeObject = await putTree({\n treeId: journeyId,\n treeData: treeObject,\n state,\n });\n return newTreeObject['enabled'] === true;\n } catch (error) {\n printMessage(error.response.data, 'error');\n return false;\n }\n}\n\n/**\n * Disable a journey\n * @param journeyId journey id/name\n * @returns {Promise<boolean>} true if the operation was successful, false otherwise\n */\nexport async function disableJourney({\n journeyId,\n state,\n}: {\n journeyId: string;\n state: State;\n}): Promise<boolean> {\n try {\n const treeObject = await getTree({ id: journeyId, state });\n treeObject['enabled'] = false;\n delete treeObject._rev;\n const newTreeObject = await putTree({\n treeId: journeyId,\n treeData: treeObject,\n state,\n });\n return newTreeObject['enabled'] === false;\n } catch (error) {\n printMessage(error.response.data, 'error');\n return false;\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/ops/JourneyOps.ts"],"names":[],"mappings":"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","file":"JourneyOps.d.ts","sourcesContent":["import fs from 'fs';\nimport { v4 as uuidv4 } from 'uuid';\nimport _ from 'lodash';\nimport {\n convertBase64TextToArray,\n getTypedFilename,\n convertTextArrayToBase64,\n convertTextArrayToBase64Url,\n findFilesByName,\n getMetadata,\n} from './utils/ExportImportUtils';\nimport { getRealmManagedUser, replaceAll } from './utils/OpsUtils';\nimport State from '../shared/State';\nimport {\n getNode,\n putNode,\n deleteNode,\n getNodeTypes,\n getNodesByType,\n} from '../api/NodeApi';\nimport { isCloudOnlyNode, isCustomNode, isPremiumNode } from './NodeOps';\nimport { getTrees, getTree, putTree, deleteTree } from '../api/TreeApi';\nimport { getEmailTemplate, putEmailTemplate } from './EmailTemplateOps';\nimport { getScript } from '../api/ScriptApi';\nimport * as globalConfig from '../storage/StaticStorage';\nimport {\n printMessage,\n createProgressIndicator,\n updateProgressIndicator,\n stopProgressIndicator,\n debugMessage,\n} from './utils/Console';\nimport {\n getProviderByLocationAndId,\n getProviders,\n getProviderMetadata,\n createProvider,\n findProviders,\n updateProvider,\n} from '../api/Saml2Api';\nimport {\n createCircleOfTrust,\n getCirclesOfTrust,\n updateCircleOfTrust,\n} from '../api/CirclesOfTrustApi';\nimport {\n decode,\n encode,\n encodeBase64Url,\n isBase64Encoded,\n} from '../api/utils/Base64';\nimport {\n getSocialIdentityProviders,\n putProviderByTypeAndId,\n} from '../api/SocialIdentityProvidersApi';\nimport { getThemes, putThemes } from './ThemeOps';\nimport { putScript } from './ScriptOps';\nimport { JourneyClassification, TreeExportResolverInterface } from './OpsTypes';\nimport {\n InnerNodeRefSkeletonInterface,\n NodeRefSkeletonInterface,\n NodeSkeleton,\n ThemeSkeleton,\n TreeSkeleton,\n} from '../api/ApiTypes';\nimport {\n SingleTreeExportInterface,\n MultiTreeExportInterface,\n TreeDependencyMapInterface,\n TreeExportOptions,\n TreeImportOptions,\n} from './OpsTypes';\n\nexport default class JourneyOps {\n state: State;\n constructor(state: State) {\n this.state = state;\n }\n\n /**\n * Create an empty single tree export template\n * @returns {SingleTreeExportInterface} an empty single tree export template\n */\n createSingleTreeExportTemplate(): SingleTreeExportInterface {\n return createSingleTreeExportTemplate({ state: this.state });\n }\n\n /**\n * Create an empty multi tree export template\n * @returns {MultiTreeExportInterface} an empty multi tree export template\n */\n createMultiTreeExportTemplate(): MultiTreeExportInterface {\n return createMultiTreeExportTemplate({ state: this.state });\n }\n\n /**\n * Create export data for a tree/journey with all its nodes and dependencies. The export data can be written to a file as is.\n * @param {string} treeId tree id/name\n * @param {TreeExportOptions} options export options\n * @returns {Promise<SingleTreeExportInterface>} a promise that resolves to an object containing the tree and all its nodes and dependencies\n */\n async exportJourney(\n treeId: string,\n options: TreeExportOptions = {\n useStringArrays: true,\n deps: true,\n }\n ): Promise<SingleTreeExportInterface> {\n return exportJourney({ treeId, options, state: this.state });\n }\n\n /**\n * Get all the journeys/trees without all their nodes and dependencies.\n * @returns {Promise<TreeSkeleton[]>} a promise that resolves to an array of journey objects\n */\n async getJourneys(): Promise<TreeSkeleton[]> {\n return getJourneys({ state: this.state });\n }\n\n /**\n * Get a journey/tree without all its nodes and dependencies.\n * @param {string} journeyId journey id/name\n * @returns {Promise<TreeSkeleton>} a promise that resolves to a journey object\n */\n async getJourney(journeyId: string): Promise<TreeSkeleton> {\n return getJourney({ journeyId, state: this.state });\n }\n\n /**\n * Helper to import a tree with all dependencies from a `SingleTreeExportInterface` object (typically read from a file)\n * @param {SingleTreeExportInterface} treeObject tree object containing tree and all its dependencies\n * @param {TreeImportOptions} options import options\n * @returns {Promise<boolean>} a promise that resolves to true if no errors occurred during import\n */\n async importJourney(\n treeObject: SingleTreeExportInterface,\n options: TreeImportOptions\n ): Promise<boolean> {\n return importJourney({ treeObject, options, state: this.state });\n }\n\n /**\n * Resolve journey dependencies\n * @param {Map} installedJorneys Map of installed journeys\n * @param {Map} journeyMap Map of journeys to resolve dependencies for\n * @param {string[]} unresolvedJourneys Map to hold the names of unresolved journeys and their dependencies\n * @param {string[]} resolvedJourneys Array to hold the names of resolved journeys\n * @param {int} index Depth of recursion\n */\n async resolveDependencies(\n installedJorneys,\n journeyMap,\n unresolvedJourneys,\n resolvedJourneys,\n index = -1\n ) {\n return resolveDependencies(\n installedJorneys,\n journeyMap,\n unresolvedJourneys,\n resolvedJourneys,\n index\n );\n }\n\n /**\n * Helper to import multiple trees from a tree map\n * @param {MultiTreeExportInterface} treesMap map of trees object\n * @param {TreeImportOptions} options import options\n */\n async importAllJourneys(\n treesMap: MultiTreeExportInterface,\n options: TreeImportOptions\n ) {\n return importAllJourneys({ treesMap, options, state: this.state });\n }\n\n /**\n * Get the node reference obbject for a node object. Node reference objects\n * are used in a tree flow definition and within page nodes to reference\n * nodes. Among other things, node references contain all the non-configuration\n * meta data that exists for readaility, like the x/y coordinates of the node\n * and the display name chosen by the tree designer. The dislay name is the\n * only intuitive link between the graphical representation of the tree and\n * the node configurations that make up the tree.\n * @param nodeObj node object to retrieve the node reference object for\n * @param singleTreeExport tree export with or without dependencies\n * @returns {NodeRefSkeletonInterface | InnerNodeRefSkeletonInterface} node reference object\n */\n getNodeRef(\n nodeObj: NodeSkeleton,\n singleTreeExport: SingleTreeExportInterface\n ): NodeRefSkeletonInterface | InnerNodeRefSkeletonInterface {\n return getNodeRef(nodeObj, singleTreeExport);\n }\n\n /**\n * Default tree export resolver used to resolve a tree id/name to a full export\n * w/o dependencies of that tree from a platform instance.\n * @param {string} treeId id/name of the tree to resolve\n * @returns {TreeExportResolverInterface} tree export\n */\n onlineTreeExportResolver: TreeExportResolverInterface =\n onlineTreeExportResolver;\n\n /**\n * Tree export resolver used to resolve a tree id/name to a full export\n * of that tree from individual `treename.journey.json` export files.\n * @param {string} treeId id/name of the tree to resolve\n * @returns {TreeExportResolverInterface} tree export\n */\n fileByIdTreeExportResolver: TreeExportResolverInterface =\n fileByIdTreeExportResolver;\n\n /**\n * Factory that creates a tree export resolver used to resolve a tree id\n * to a full export of that tree from a multi-tree export file.\n * @param {string} file multi-tree export file\n * @returns {TreeExportResolverInterface} tree export resolver\n */\n createFileParamTreeExportResolver(file: string): TreeExportResolverInterface {\n return createFileParamTreeExportResolver(file, this.state);\n }\n\n /**\n * Get tree dependencies (all descendent inner trees)\n * @param {SingleTreeExportInterface} treeExport single tree export\n * @param {string[]} resolvedTreeIds list of tree ids wich have already been resolved\n * @param {TreeExportResolverInterface} resolveTreeExport tree export resolver callback function\n * @returns {Promise<TreeDependencyMapInterface>} a promise that resolves to a tree dependency map\n */\n async getTreeDescendents(\n treeExport: SingleTreeExportInterface,\n resolveTreeExport: TreeExportResolverInterface,\n resolvedTreeIds: string[] = []\n ): Promise<TreeDependencyMapInterface> {\n return getTreeDescendents({\n treeExport,\n resolveTreeExport,\n resolvedTreeIds,\n state: this.state,\n });\n }\n\n /**\n * Find all node configuration objects that are no longer referenced by any tree\n * @returns {Promise<unknown[]>} a promise that resolves to an array of orphaned nodes\n */\n async findOrphanedNodes(): Promise<NodeSkeleton[]> {\n return findOrphanedNodes({ state: this.state });\n }\n\n /**\n * Remove orphaned nodes\n * @param {NodeSkeleton[]} orphanedNodes Pass in an array of orphaned node configuration objects to remove\n * @returns {Promise<NodeSkeleton[]>} a promise that resolves to an array nodes that encountered errors deleting\n */\n async removeOrphanedNodes(orphanedNodes: NodeSkeleton[]): Promise<unknown[]> {\n return removeOrphanedNodes({ orphanedNodes, state: this.state });\n }\n\n /**\n * Analyze if a journey contains any custom nodes considering the detected or the overridden version.\n * @param {SingleTreeExportInterface} journey Journey/tree configuration object\n * @returns {boolean} True if the journey/tree contains any custom nodes, false otherwise.\n */\n isCustomJourney(journey: SingleTreeExportInterface) {\n return isCustomJourney({ journey, state: this.state });\n }\n\n /**\n * Analyze if a journey contains any premium nodes considering the detected or the overridden version.\n * @param {SingleTreeExportInterface} journey Journey/tree configuration object\n * @returns {boolean} True if the journey/tree contains any custom nodes, false otherwise.\n */\n isPremiumJourney(journey: SingleTreeExportInterface) {\n return isPremiumJourney(journey);\n }\n\n /**\n * Analyze if a journey contains any cloud-only nodes considering the detected or the overridden version.\n * @param {SingleTreeExportInterface} journey Journey/tree configuration object\n * @returns {boolean} True if the journey/tree contains any cloud-only nodes, false otherwise.\n */\n isCloudOnlyJourney(journey: SingleTreeExportInterface) {\n return isCloudOnlyJourney(journey);\n }\n\n /**\n * Get a journey's classifications, which can be one or multiple of:\n * - standard: can run on any instance of a ForgeRock platform\n * - cloud: utilize nodes, which are exclusively available in the ForgeRock Identity Cloud\n * - premium: utilizes nodes, which come at a premium\n * - custom: utilizes nodes not included in the ForgeRock platform release\n * @param {SingleTreeExportInterface} journey journey export data\n * @returns {JourneyClassification[]} an array of one or multiple classifications\n */\n getJourneyClassification(\n journey: SingleTreeExportInterface\n ): JourneyClassification[] {\n return getJourneyClassification({ journey, state: this.state });\n }\n\n /**\n * Delete a journey\n * @param {string} journeyId journey id/name\n * @param {Object} options deep=true also delete all the nodes and inner nodes, verbose=true print verbose info\n */\n async deleteJourney(\n journeyId: string,\n options: { deep: boolean; verbose: boolean; progress?: boolean }\n ) {\n return deleteJourney({ journeyId, options, state: this.state });\n }\n\n /**\n * Delete all journeys\n * @param {Object} options deep=true also delete all the nodes and inner nodes, verbose=true print verbose info\n */\n async deleteJourneys(options: { deep: boolean; verbose: boolean }) {\n return deleteJourneys({ options, state: this.state });\n }\n\n /**\n * Enable a journey\n * @param journeyId journey id/name\n * @returns {Promise<boolean>} true if the operation was successful, false otherwise\n */\n async enableJourney(journeyId: string): Promise<boolean> {\n return enableJourney({ journeyId, state: this.state });\n }\n\n /**\n * Disable a journey\n * @param journeyId journey id/name\n * @returns {Promise<boolean>} true if the operation was successful, false otherwise\n */\n async disableJourney(journeyId: string): Promise<boolean> {\n return disableJourney({ journeyId, state: this.state });\n }\n}\n\nconst containerNodes = ['PageNode', 'CustomPageNode'];\n\nconst scriptedNodes = [\n 'ConfigProviderNode',\n 'ScriptedDecisionNode',\n 'ClientScriptNode',\n 'SocialProviderHandlerNode',\n 'CustomScriptNode',\n];\n\nconst emailTemplateNodes = ['EmailSuspendNode', 'EmailTemplateNode'];\n\nconst emptyScriptPlaceholder = '[Empty]';\n\n/**\n * Create an empty single tree export template\n * @returns {SingleTreeExportInterface} an empty single tree export template\n */\nfunction createSingleTreeExportTemplate({\n state,\n}: {\n state: State;\n}): SingleTreeExportInterface {\n return {\n meta: getMetadata({ state }),\n innerNodes: {},\n nodes: {},\n scripts: {},\n emailTemplates: {},\n socialIdentityProviders: {},\n themes: [],\n saml2Entities: {},\n circlesOfTrust: {},\n tree: {},\n } as SingleTreeExportInterface;\n}\n\n/**\n * Create an empty multi tree export template\n * @returns {MultiTreeExportInterface} an empty multi tree export template\n */\nfunction createMultiTreeExportTemplate({\n state,\n}: {\n state: State;\n}): MultiTreeExportInterface {\n return {\n meta: getMetadata({ state }),\n trees: {},\n } as MultiTreeExportInterface;\n}\n\n/**\n * Helper to get all SAML2 dependencies for a given node object\n * @param {Object} nodeObject node object\n * @param {[Object]} allProviders array of all saml2 providers objects\n * @param {[Object]} allCirclesOfTrust array of all circle of trust objects\n * @returns {Promise} a promise that resolves to an object containing a saml2 dependencies\n */\nasync function getSaml2NodeDependencies(\n nodeObject,\n allProviders,\n allCirclesOfTrust,\n state: State\n) {\n const samlProperties = ['metaAlias', 'idpEntityId'];\n const saml2EntityPromises = [];\n for (const samlProperty of samlProperties) {\n // In the following line nodeObject[samlProperty] will look like '/alpha/iSPAzure'.\n const entityId =\n samlProperty === 'metaAlias'\n ? _.last(nodeObject[samlProperty].split('/'))\n : nodeObject[samlProperty];\n const entity = _.find(allProviders, { entityId });\n if (entity) {\n try {\n const providerResponse = await getProviderByLocationAndId({\n location: entity.location,\n entityId64: entity._id,\n state,\n });\n /**\n * Adding entityLocation here to the entityResponse because the import tool\n * needs to know whether the saml2 entity is remote or not (this will be removed\n * from the config before importing see updateSaml2Entity and createSaml2Entity functions).\n * Importing a remote saml2 entity is a slightly different request (see createSaml2Entity).\n */\n providerResponse.entityLocation = entity.location;\n\n if (entity.location === 'remote') {\n // get the xml representation of this entity and add it to the entityResponse;\n const metaDataResponse = await getProviderMetadata({\n entityId: providerResponse.entityId,\n state,\n });\n providerResponse.base64EntityXML = encodeBase64Url(metaDataResponse);\n }\n saml2EntityPromises.push(providerResponse);\n } catch (error) {\n printMessage(error.message, 'error');\n }\n }\n }\n try {\n const saml2EntitiesPromisesResults = await Promise.all(saml2EntityPromises);\n const saml2Entities = [];\n for (const saml2Entity of saml2EntitiesPromisesResults) {\n if (saml2Entity) {\n saml2Entities.push(saml2Entity);\n }\n }\n const samlEntityIds = _.map(\n saml2Entities,\n (saml2EntityConfig) => `${saml2EntityConfig.entityId}|saml2`\n );\n const circlesOfTrust = _.filter(allCirclesOfTrust, (circleOfTrust) => {\n let hasEntityId = false;\n for (const trustedProvider of circleOfTrust.trustedProviders) {\n if (!hasEntityId && samlEntityIds.includes(trustedProvider)) {\n hasEntityId = true;\n }\n }\n return hasEntityId;\n });\n const saml2NodeDependencies = {\n saml2Entities,\n circlesOfTrust,\n };\n return saml2NodeDependencies;\n } catch (error) {\n printMessage(error.message, 'error');\n const saml2NodeDependencies = {\n saml2Entities: [],\n circlesOfTrust: [],\n };\n return saml2NodeDependencies;\n }\n}\n\n/**\n * Create export data for a tree/journey with all its nodes and dependencies. The export data can be written to a file as is.\n * @param {string} treeId tree id/name\n * @param {TreeExportOptions} options export options\n * @returns {Promise<SingleTreeExportInterface>} a promise that resolves to an object containing the tree and all its nodes and dependencies\n */\nexport async function exportJourney({\n treeId,\n options = {\n useStringArrays: true,\n deps: true,\n },\n state,\n}: {\n treeId: string;\n options?: TreeExportOptions;\n state: State;\n}): Promise<SingleTreeExportInterface> {\n const exportData = createSingleTreeExportTemplate({ state });\n try {\n const treeObject = await getTree({ id: treeId, state });\n const { useStringArrays, deps } = options;\n const verbose = state.getDebug();\n\n if (verbose) printMessage(`\\n- ${treeObject._id}\\n`, 'info', false);\n\n // Process tree\n if (verbose) printMessage(' - Flow');\n exportData.tree = treeObject;\n if (verbose && treeObject.identityResource)\n printMessage(\n ` - identityResource: ${treeObject.identityResource}`,\n 'info'\n );\n if (verbose) printMessage(` - Done`, 'info');\n\n const nodePromises = [];\n const scriptPromises = [];\n const emailTemplatePromises = [];\n const innerNodePromises = [];\n const saml2ConfigPromises = [];\n let socialProviderPromise = null;\n let themePromise = null;\n if (\n deps &&\n state.getDeploymentType() !== globalConfig.CLASSIC_DEPLOYMENT_TYPE_KEY\n ) {\n try {\n themePromise = getThemes({ state });\n } catch (error) {\n printMessage(error, 'error');\n }\n }\n\n let allSaml2Providers = null;\n let allCirclesOfTrust = null;\n let filteredSocialProviders = null;\n const themes = [];\n\n // get all the nodes\n for (const [nodeId, nodeInfo] of Object.entries(treeObject.nodes)) {\n nodePromises.push(\n getNode({ nodeId, nodeType: nodeInfo['nodeType'], state })\n );\n }\n if (verbose && nodePromises.length > 0) printMessage(' - Nodes:');\n const nodeObjects = await Promise.all(nodePromises);\n\n // iterate over every node in tree\n for (const nodeObject of nodeObjects) {\n const nodeId = nodeObject._id;\n const nodeType = nodeObject._type._id;\n if (verbose) printMessage(` - ${nodeId} (${nodeType})`, 'info', true);\n exportData.nodes[nodeObject._id] = nodeObject;\n\n // handle script node types\n if (\n deps &&\n scriptedNodes.includes(nodeType) &&\n nodeObject.script !== emptyScriptPlaceholder\n ) {\n scriptPromises.push(getScript({ scriptId: nodeObject.script, state }));\n }\n\n // frodo supports email templates in platform deployments\n if (\n (deps &&\n state.getDeploymentType() ===\n globalConfig.CLOUD_DEPLOYMENT_TYPE_KEY) ||\n state.getDeploymentType() === globalConfig.FORGEOPS_DEPLOYMENT_TYPE_KEY\n ) {\n if (emailTemplateNodes.includes(nodeType)) {\n try {\n const emailTemplate = await getEmailTemplate({\n templateId: nodeObject.emailTemplateName,\n state,\n });\n emailTemplatePromises.push(emailTemplate);\n } catch (error) {\n let message = `${error}`;\n if (error.isAxiosError && error.response.status) {\n message = error.response.statusText;\n }\n printMessage(\n `\\n${message}: Email Template \"${nodeObject.emailTemplateName}\"`,\n 'error'\n );\n }\n }\n }\n\n // handle SAML2 node dependencies\n if (deps && nodeType === 'product-Saml2Node') {\n if (!allSaml2Providers) {\n // eslint-disable-next-line no-await-in-loop\n allSaml2Providers = (await getProviders({ state })).result;\n }\n if (!allCirclesOfTrust) {\n // eslint-disable-next-line no-await-in-loop\n allCirclesOfTrust = (await getCirclesOfTrust({ state })).result;\n }\n saml2ConfigPromises.push(\n getSaml2NodeDependencies(\n nodeObject,\n allSaml2Providers,\n allCirclesOfTrust,\n state\n )\n );\n }\n\n // If this is a SocialProviderHandlerNode get each enabled social identity provider.\n if (\n deps &&\n !socialProviderPromise &&\n nodeType === 'SocialProviderHandlerNode'\n ) {\n socialProviderPromise = getSocialIdentityProviders({ state });\n }\n\n // If this is a SelectIdPNode and filteredProviters is not already set to empty array set filteredSocialProviers.\n if (deps && !filteredSocialProviders && nodeType === 'SelectIdPNode') {\n filteredSocialProviders = filteredSocialProviders || [];\n for (const filteredProvider of nodeObject.filteredProviders) {\n if (!filteredSocialProviders.includes(filteredProvider)) {\n filteredSocialProviders.push(filteredProvider);\n }\n }\n }\n\n // get inner nodes (nodes inside container nodes)\n if (containerNodes.includes(nodeType)) {\n for (const innerNode of nodeObject.nodes) {\n innerNodePromises.push(\n getNode({\n nodeId: innerNode._id,\n nodeType: innerNode.nodeType,\n state,\n })\n );\n }\n // frodo supports themes in platform deployments\n if (\n (deps &&\n state.getDeploymentType() ===\n globalConfig.CLOUD_DEPLOYMENT_TYPE_KEY) ||\n state.getDeploymentType() ===\n globalConfig.FORGEOPS_DEPLOYMENT_TYPE_KEY\n ) {\n let themeId = false;\n\n if (nodeObject.stage) {\n // see if themeId is part of the stage object\n try {\n themeId = JSON.parse(nodeObject.stage).themeId;\n } catch (e) {\n themeId = false;\n }\n // if the page node's themeId is set the \"old way\" set themeId accordingly\n if (!themeId && nodeObject.stage.indexOf('themeId=') === 0) {\n // eslint-disable-next-line prefer-destructuring\n themeId = nodeObject.stage.split('=')[1];\n }\n }\n\n if (themeId) {\n if (!themes.includes(themeId)) themes.push(themeId);\n }\n }\n }\n }\n\n // Process inner nodes\n if (verbose && innerNodePromises.length > 0)\n printMessage(' - Inner nodes:');\n const innerNodeDataResults = await Promise.all(innerNodePromises);\n for (const innerNodeObject of innerNodeDataResults) {\n const innerNodeId = innerNodeObject._id;\n const innerNodeType = innerNodeObject._type._id;\n if (verbose)\n printMessage(` - ${innerNodeId} (${innerNodeType})`, 'info', true);\n exportData.innerNodes[innerNodeId] = innerNodeObject;\n\n // handle script node types\n if (deps && scriptedNodes.includes(innerNodeType)) {\n scriptPromises.push(\n getScript({ scriptId: innerNodeObject.script, state })\n );\n }\n\n // frodo supports email templates in platform deployments\n if (\n (deps &&\n state.getDeploymentType() ===\n globalConfig.CLOUD_DEPLOYMENT_TYPE_KEY) ||\n state.getDeploymentType() === globalConfig.FORGEOPS_DEPLOYMENT_TYPE_KEY\n ) {\n if (emailTemplateNodes.includes(innerNodeType)) {\n try {\n const emailTemplate = await getEmailTemplate({\n templateId: innerNodeObject.emailTemplateName,\n state,\n });\n emailTemplatePromises.push(emailTemplate);\n } catch (error) {\n let message = `${error}`;\n if (error.isAxiosError && error.response.status) {\n message = error.response.statusText;\n }\n printMessage(\n `\\n${message}: Email Template \"${innerNodeObject.emailTemplateName}\"`,\n 'error'\n );\n }\n }\n }\n\n // handle SAML2 node dependencies\n if (deps && innerNodeType === 'product-Saml2Node') {\n printMessage('SAML2 inner node', 'error');\n if (!allSaml2Providers) {\n // eslint-disable-next-line no-await-in-loop\n allSaml2Providers = (await getProviders({ state })).result;\n }\n if (!allCirclesOfTrust) {\n // eslint-disable-next-line no-await-in-loop\n allCirclesOfTrust = (await getCirclesOfTrust({ state })).result;\n }\n saml2ConfigPromises.push(\n getSaml2NodeDependencies(\n innerNodeObject,\n allSaml2Providers,\n allCirclesOfTrust,\n state\n )\n );\n }\n\n // If this is a SocialProviderHandlerNode get each enabled social identity provider.\n if (\n deps &&\n !socialProviderPromise &&\n innerNodeType === 'SocialProviderHandlerNode'\n ) {\n socialProviderPromise = getSocialIdentityProviders({ state });\n }\n\n // If this is a SelectIdPNode and filteredProviters is not already set to empty array set filteredSocialProviers.\n if (\n deps &&\n !filteredSocialProviders &&\n innerNodeType === 'SelectIdPNode' &&\n innerNodeObject.filteredProviders\n ) {\n filteredSocialProviders = filteredSocialProviders || [];\n for (const filteredProvider of innerNodeObject.filteredProviders) {\n if (!filteredSocialProviders.includes(filteredProvider)) {\n filteredSocialProviders.push(filteredProvider);\n }\n }\n }\n }\n\n // Process email templates\n if (verbose && emailTemplatePromises.length > 0)\n printMessage(' - Email templates:');\n const settledEmailTemplatePromises = await Promise.allSettled(\n emailTemplatePromises\n );\n for (const settledPromise of settledEmailTemplatePromises) {\n if (settledPromise.status === 'fulfilled' && settledPromise.value) {\n if (verbose)\n printMessage(\n ` - ${settledPromise.value._id.split('/')[1]}${\n settledPromise.value.displayName\n ? ` (${settledPromise.value.displayName})`\n : ''\n }`,\n 'info',\n true\n );\n exportData.emailTemplates[settledPromise.value._id.split('/')[1]] =\n settledPromise.value;\n }\n }\n\n // Process SAML2 providers and circles of trust\n const saml2NodeDependencies = await Promise.all(saml2ConfigPromises);\n for (const saml2NodeDependency of saml2NodeDependencies) {\n if (saml2NodeDependency) {\n if (verbose) printMessage(' - SAML2 entity providers:');\n for (const saml2Entity of saml2NodeDependency.saml2Entities) {\n if (verbose)\n printMessage(\n ` - ${saml2Entity.entityLocation} ${saml2Entity.entityId}`,\n 'info'\n );\n exportData.saml2Entities[saml2Entity._id] = saml2Entity;\n }\n if (verbose) printMessage(' - SAML2 circles of trust:');\n for (const circleOfTrust of saml2NodeDependency.circlesOfTrust) {\n if (verbose) printMessage(` - ${circleOfTrust._id}`, 'info');\n exportData.circlesOfTrust[circleOfTrust._id] = circleOfTrust;\n }\n }\n }\n\n // Process socialIdentityProviders\n const socialProviders = await Promise.resolve(socialProviderPromise);\n if (socialProviders) {\n if (verbose) printMessage(' - OAuth2/OIDC (social) identity providers:');\n for (const socialProvider of socialProviders.result) {\n // If the list of socialIdentityProviders needs to be filtered based on the\n // filteredProviders property of a SelectIdPNode do it here.\n if (\n socialProvider &&\n (!filteredSocialProviders ||\n filteredSocialProviders.length === 0 ||\n filteredSocialProviders.includes(socialProvider._id))\n ) {\n if (verbose) printMessage(` - ${socialProvider._id}`, 'info');\n scriptPromises.push(\n getScript({ scriptId: socialProvider.transform, state })\n );\n exportData.socialIdentityProviders[socialProvider._id] =\n socialProvider;\n }\n }\n }\n\n // Process scripts\n if (verbose && scriptPromises.length > 0) printMessage(' - Scripts:');\n const scriptObjects = await Promise.all(scriptPromises);\n for (const scriptObject of scriptObjects) {\n if (scriptObject) {\n if (verbose)\n printMessage(\n ` - ${scriptObject._id} (${scriptObject.name})`,\n 'info',\n true\n );\n scriptObject.script = useStringArrays\n ? convertBase64TextToArray(scriptObject.script)\n : JSON.stringify(decode(scriptObject.script));\n exportData.scripts[scriptObject._id] = scriptObject;\n }\n }\n\n // Process themes\n if (themePromise) {\n if (verbose) printMessage(' - Themes:');\n try {\n const themePromiseResults = await Promise.resolve(themePromise);\n for (const themeObject of themePromiseResults) {\n if (\n themeObject &&\n // has the theme been specified by id or name in a page node?\n (themes.includes(themeObject._id) ||\n themes.includes(themeObject.name) ||\n // has this journey been linked to a theme?\n themeObject.linkedTrees?.includes(treeObject._id))\n ) {\n if (verbose)\n printMessage(\n ` - ${themeObject._id} (${themeObject.name})`,\n 'info'\n );\n exportData.themes.push(themeObject);\n }\n }\n } catch (error) {\n printMessage(error.response.data, 'error');\n printMessage('Error handling themes: ' + error.message, 'error');\n }\n }\n } catch (error) {\n printMessage(error.response.data, 'error');\n printMessage(\n 'Error exporting tree: ' + treeId + ' - ' + error.message,\n 'error'\n );\n }\n\n return exportData;\n}\n\n/**\n * Get all the journeys/trees without all their nodes and dependencies.\n * @returns {Promise<TreeSkeleton[]>} a promise that resolves to an array of journey objects\n */\nexport async function getJourneys({\n state,\n}: {\n state: State;\n}): Promise<TreeSkeleton[]> {\n const { result } = await getTrees({ state });\n result.sort((a, b) => a._id.localeCompare(b._id));\n return result;\n}\n\n/**\n * Get a journey/tree without all its nodes and dependencies.\n * @param {string} journeyId journey id/name\n * @returns {Promise<TreeSkeleton>} a promise that resolves to a journey object\n */\nexport async function getJourney({\n journeyId,\n state,\n}: {\n journeyId: string;\n state: State;\n}): Promise<TreeSkeleton> {\n const response = await getTree({ id: journeyId, state });\n return response;\n}\n\n/**\n * Helper to import a tree with all dependencies from a `SingleTreeExportInterface` object (typically read from a file)\n * @param {SingleTreeExportInterface} treeObject tree object containing tree and all its dependencies\n * @param {TreeImportOptions} options import options\n * @returns {Promise<boolean>} a promise that resolves to true if no errors occurred during import\n */\nexport async function importJourney({\n treeObject,\n options,\n state,\n}: {\n treeObject: SingleTreeExportInterface;\n options: TreeImportOptions;\n state: State;\n}): Promise<boolean> {\n const { reUuid, deps } = options;\n const verbose = state.getDebug();\n if (verbose) printMessage(`\\n- ${treeObject.tree._id}\\n`, 'info', false);\n let newUuid = '';\n const uuidMap = {};\n const treeId = treeObject.tree._id;\n\n // Process scripts\n if (\n deps &&\n treeObject.scripts &&\n Object.entries(treeObject.scripts).length > 0\n ) {\n if (verbose) printMessage(' - Scripts:');\n for (const [scriptId, scriptObject] of Object.entries(treeObject.scripts)) {\n if (verbose)\n printMessage(\n ` - ${scriptId} (${scriptObject['name']})`,\n 'info',\n false\n );\n // is the script stored as an array of strings or just b64 blob?\n if (Array.isArray(scriptObject['script'])) {\n scriptObject['script'] = convertTextArrayToBase64(\n scriptObject['script']\n );\n } else if (!isBase64Encoded(scriptObject['script'])) {\n scriptObject['script'] = encode(JSON.parse(scriptObject['script']));\n }\n try {\n await putScript({ scriptId, scriptData: scriptObject, state });\n } catch (error) {\n throw new Error(\n `Error importing script ${scriptObject['name']} (${scriptId}) in journey ${treeId}: ${error.message}`\n );\n }\n if (verbose) printMessage('');\n }\n }\n\n // Process email templates\n if (\n deps &&\n treeObject.emailTemplates &&\n Object.entries(treeObject.emailTemplates).length > 0\n ) {\n if (verbose) printMessage(' - Email templates:');\n for (const [templateId, templateData] of Object.entries(\n treeObject.emailTemplates\n )) {\n if (verbose) printMessage(` - ${templateId}`, 'info', false);\n try {\n await putEmailTemplate({ templateId, templateData, state });\n } catch (error) {\n printMessage(error.response.data, 'error');\n throw new Error(`Error importing email templates: ${error.message}`);\n }\n if (verbose) printMessage('');\n }\n }\n\n // Process themes\n if (deps && treeObject.themes && treeObject.themes.length > 0) {\n if (verbose) printMessage(' - Themes:');\n const themes: Map<string, ThemeSkeleton> = new Map<string, ThemeSkeleton>();\n for (const theme of treeObject.themes) {\n if (verbose)\n printMessage(` - ${theme['_id']} (${theme['name']})`, 'info');\n themes[theme['_id']] = theme;\n }\n try {\n await putThemes({ themeMap: themes, state });\n } catch (error) {\n throw new Error(`Error importing themes: ${error.message}`);\n }\n }\n\n // Process social providers\n if (\n deps &&\n treeObject.socialIdentityProviders &&\n Object.entries(treeObject.socialIdentityProviders).length > 0\n ) {\n if (verbose) printMessage(' - OAuth2/OIDC (social) identity providers:');\n for (const [providerId, providerData] of Object.entries(\n treeObject.socialIdentityProviders\n )) {\n if (verbose) printMessage(` - ${providerId}`, 'info');\n try {\n await putProviderByTypeAndId({\n type: providerData['_type']['_id'],\n id: providerId,\n providerData,\n state,\n });\n } catch (importError) {\n if (\n importError.response?.status === 500 &&\n importError.response?.data?.message ===\n 'Unable to update SMS config: Data validation failed for the attribute, Redirect after form post URL'\n ) {\n providerData['redirectAfterFormPostURI'] = '';\n try {\n await putProviderByTypeAndId({\n type: providerData['_type']['_id'],\n id: providerId,\n providerData,\n state,\n });\n } catch (importError2) {\n printMessage(importError.response?.data || importError, 'error');\n throw new Error(\n `Error importing provider ${providerId} in journey ${treeId}: ${importError}`\n );\n }\n } else {\n printMessage(importError.response?.data || importError, 'error');\n throw new Error(\n `\\nError importing provider ${providerId} in journey ${treeId}: ${importError}`\n );\n }\n }\n }\n }\n\n // Process saml providers\n if (\n deps &&\n treeObject.saml2Entities &&\n Object.entries(treeObject.saml2Entities).length > 0\n ) {\n if (verbose) printMessage(' - SAML2 entity providers:');\n for (const [, providerData] of Object.entries(treeObject.saml2Entities)) {\n delete providerData['_rev'];\n const entityId = providerData['entityId'];\n const entityLocation = providerData['entityLocation'];\n if (verbose) printMessage(` - ${entityLocation} ${entityId}`, 'info');\n let metaData = null;\n if (entityLocation === 'remote') {\n if (Array.isArray(providerData['base64EntityXML'])) {\n metaData = convertTextArrayToBase64Url(\n providerData['base64EntityXML']\n );\n } else {\n metaData = providerData['base64EntityXML'];\n }\n }\n delete providerData['entityLocation'];\n delete providerData['base64EntityXML'];\n // create the provider if it doesn't already exist, or just update it\n if (\n (\n await findProviders({\n filter: `entityId eq '${entityId}'`,\n fields: ['location'],\n state,\n })\n ).resultCount === 0\n ) {\n await createProvider({\n location: entityLocation,\n providerData,\n metaData,\n state,\n }).catch((createProviderErr) => {\n printMessage(\n createProviderErr.response?.data || createProviderErr,\n 'error'\n );\n throw new Error(`Error creating provider ${entityId}`);\n });\n } else {\n await updateProvider({\n location: entityLocation,\n providerData,\n state,\n }).catch((updateProviderErr) => {\n printMessage(\n updateProviderErr.response?.data || updateProviderErr,\n 'error'\n );\n throw new Error(`Error updating provider ${entityId}`);\n });\n }\n }\n }\n\n // Process circles of trust\n if (\n deps &&\n treeObject.circlesOfTrust &&\n Object.entries(treeObject.circlesOfTrust).length > 0\n ) {\n if (verbose) printMessage(' - SAML2 circles of trust:');\n for (const [cotId, cotData] of Object.entries(treeObject.circlesOfTrust)) {\n delete cotData['_rev'];\n if (verbose) printMessage(` - ${cotId}`, 'info');\n try {\n await createCircleOfTrust({ cotData, state });\n } catch (createCotErr) {\n if (\n createCotErr.response?.status === 409 ||\n createCotErr.response?.status === 500\n ) {\n try {\n await updateCircleOfTrust({ cotId, cotData, state });\n } catch (updateCotErr) {\n printMessage(createCotErr.response?.data || createCotErr, 'error');\n printMessage(updateCotErr.response?.data || updateCotErr, 'error');\n throw new Error(`Error creating/updating circle of trust ${cotId}`);\n }\n } else {\n printMessage(createCotErr.response?.data || createCotErr, 'error');\n throw new Error(`Error creating circle of trust ${cotId}`);\n }\n }\n }\n }\n\n // Process inner nodes\n let innerNodes = {};\n if (\n treeObject.innerNodes &&\n Object.entries(treeObject.innerNodes).length > 0\n ) {\n innerNodes = treeObject.innerNodes;\n }\n // old export file format\n else if (\n treeObject.innernodes &&\n Object.entries(treeObject.innernodes).length > 0\n ) {\n innerNodes = treeObject.innernodes;\n }\n if (Object.entries(innerNodes).length > 0) {\n if (verbose) printMessage(' - Inner nodes:', 'text', true);\n for (const [innerNodeId, innerNodeData] of Object.entries(innerNodes)) {\n delete innerNodeData['_rev'];\n const nodeType = innerNodeData['_type']['_id'];\n if (!reUuid) {\n newUuid = innerNodeId;\n } else {\n newUuid = uuidv4();\n uuidMap[innerNodeId] = newUuid;\n }\n innerNodeData['_id'] = newUuid;\n\n if (verbose)\n printMessage(\n ` - ${newUuid}${reUuid ? '*' : ''} (${nodeType})`,\n 'info',\n false\n );\n\n // If the node has an identityResource config setting\n // and the identityResource ends in 'user'\n // and the node's identityResource is the same as the tree's identityResource\n // change it to the current realm managed user identityResource otherwise leave it alone.\n if (\n innerNodeData['identityResource'] &&\n innerNodeData['identityResource'].endsWith('user') &&\n innerNodeData['identityResource'] === treeObject.tree.identityResource\n ) {\n innerNodeData['identityResource'] = `managed/${getRealmManagedUser({\n state,\n })}`;\n if (verbose)\n printMessage(\n `\\n - identityResource: ${innerNodeData['identityResource']}`,\n 'info',\n false\n );\n }\n try {\n await putNode({\n nodeId: newUuid,\n nodeType,\n nodeData: innerNodeData as NodeSkeleton,\n state,\n });\n } catch (nodeImportError) {\n if (\n nodeImportError.response.status === 400 &&\n nodeImportError.response.data.message ===\n 'Data validation failed for the attribute, Script'\n ) {\n throw new Error(\n `Missing script ${\n innerNodeData['script']\n } referenced by inner node ${innerNodeId}${\n innerNodeId === newUuid ? '' : ` [${newUuid}]`\n } (${innerNodeData['_type']['_id']}) in journey ${treeId}.`\n );\n } else if (\n nodeImportError.response?.status === 400 &&\n nodeImportError.response?.data?.message ===\n 'Invalid attribute specified.'\n ) {\n const { validAttributes } = nodeImportError.response.data.detail;\n validAttributes.push('_id');\n for (const attribute of Object.keys(innerNodeData)) {\n if (!validAttributes.includes(attribute)) {\n if (verbose)\n printMessage(\n `\\n - Removing invalid attribute: ${attribute}`,\n 'warn',\n false\n );\n delete innerNodeData[attribute];\n }\n }\n try {\n await putNode({\n nodeId: newUuid,\n nodeType,\n nodeData: innerNodeData as NodeSkeleton,\n state,\n });\n } catch (nodeImportError2) {\n printMessage(nodeImportError2.response.data, 'error');\n throw new Error(\n `Error importing node ${innerNodeId}${\n innerNodeId === newUuid ? '' : ` [${newUuid}]`\n } in journey ${treeId}`\n );\n }\n } else {\n printMessage(nodeImportError.response.data, 'error');\n throw new Error(\n `Error importing inner node ${innerNodeId}${\n innerNodeId === newUuid ? '' : ` [${newUuid}]`\n } in journey ${treeId}`\n );\n }\n }\n if (verbose) printMessage('');\n }\n }\n\n // Process nodes\n if (treeObject.nodes && Object.entries(treeObject.nodes).length > 0) {\n if (verbose) printMessage(' - Nodes:');\n // eslint-disable-next-line prefer-const\n for (let [nodeId, nodeData] of Object.entries(treeObject.nodes)) {\n delete nodeData['_rev'];\n const nodeType = nodeData['_type']['_id'];\n if (!reUuid) {\n newUuid = nodeId;\n } else {\n newUuid = uuidv4();\n uuidMap[nodeId] = newUuid;\n }\n nodeData['_id'] = newUuid;\n\n if (nodeType === 'PageNode' && reUuid) {\n for (const [, inPageNodeData] of Object.entries(nodeData['nodes'])) {\n const currentId = inPageNodeData['_id'];\n nodeData = JSON.parse(\n replaceAll(JSON.stringify(nodeData), currentId, uuidMap[currentId])\n );\n }\n }\n\n if (verbose)\n printMessage(\n ` - ${newUuid}${reUuid ? '*' : ''} (${nodeType})`,\n 'info',\n false\n );\n\n // If the node has an identityResource config setting\n // and the identityResource ends in 'user'\n // and the node's identityResource is the same as the tree's identityResource\n // change it to the current realm managed user identityResource otherwise leave it alone.\n if (\n nodeData.identityResource &&\n nodeData.identityResource.endsWith('user') &&\n nodeData.identityResource === treeObject.tree.identityResource\n ) {\n nodeData['identityResource'] = `managed/${getRealmManagedUser({\n state,\n })}`;\n if (verbose)\n printMessage(\n `\\n - identityResource: ${nodeData['identityResource']}`,\n 'info',\n false\n );\n }\n try {\n await putNode({ nodeId: newUuid, nodeType, nodeData, state });\n } catch (nodeImportError) {\n if (\n nodeImportError.response.status === 400 &&\n nodeImportError.response.data.message ===\n 'Data validation failed for the attribute, Script'\n ) {\n throw new Error(\n `Missing script ${nodeData['script']} referenced by node ${nodeId}${\n nodeId === newUuid ? '' : ` [${newUuid}]`\n } (${nodeData['_type']['_id']}) in journey ${treeId}.`\n );\n } else if (\n nodeImportError.response?.status === 400 &&\n nodeImportError.response?.data?.message ===\n 'Invalid attribute specified.'\n ) {\n const { validAttributes } = nodeImportError.response.data.detail;\n validAttributes.push('_id');\n for (const attribute of Object.keys(nodeData)) {\n if (!validAttributes.includes(attribute)) {\n if (verbose)\n printMessage(\n `\\n - Removing invalid attribute: ${attribute}`,\n 'warn',\n false\n );\n delete nodeData[attribute];\n }\n }\n try {\n await putNode({ nodeId: newUuid, nodeType, nodeData, state });\n } catch (nodeImportError2) {\n printMessage(nodeImportError2.response.data, 'error');\n throw new Error(\n `Error importing node ${nodeId}${\n nodeId === newUuid ? '' : ` [${newUuid}]`\n } in journey ${treeId}`\n );\n }\n } else {\n printMessage(nodeImportError.response.data, 'error');\n throw new Error(\n `Error importing node ${nodeId}${\n nodeId === newUuid ? '' : ` [${newUuid}]`\n } in journey ${treeId}`\n );\n }\n }\n if (verbose) printMessage('');\n }\n }\n\n // Process tree\n if (verbose) printMessage(' - Flow');\n\n if (reUuid) {\n let journeyText = JSON.stringify(treeObject.tree, null, 2);\n for (const [oldId, newId] of Object.entries(uuidMap)) {\n journeyText = replaceAll(journeyText, oldId, newId);\n }\n treeObject.tree = JSON.parse(journeyText);\n }\n\n // If the tree has an identityResource config setting\n // and the identityResource ends in 'user'\n // Set the identityResource for the tree to the selected resource.\n if (\n (treeObject.tree.identityResource &&\n (treeObject.tree['identityResource'] as string).endsWith('user')) ||\n state.getDeploymentType() === globalConfig.CLOUD_DEPLOYMENT_TYPE_KEY ||\n state.getDeploymentType() === globalConfig.FORGEOPS_DEPLOYMENT_TYPE_KEY\n ) {\n treeObject.tree.identityResource = `managed/${getRealmManagedUser({\n state,\n })}`;\n if (verbose)\n printMessage(\n ` - identityResource: ${treeObject.tree.identityResource}`,\n 'info',\n false\n );\n }\n\n delete treeObject.tree._rev;\n try {\n await putTree({\n treeId: treeObject.tree._id as string,\n treeData: treeObject.tree,\n state,\n });\n if (verbose) printMessage(`\\n - Done`, 'info', true);\n } catch (importError) {\n if (\n importError.response?.status === 400 &&\n importError.response?.data?.message === 'Invalid attribute specified.'\n ) {\n const { validAttributes } = importError.response.data.detail;\n validAttributes.push('_id');\n for (const attribute of Object.keys(treeObject.tree)) {\n if (!validAttributes.includes(attribute)) {\n if (verbose)\n printMessage(\n `\\n - Removing invalid attribute: ${attribute}`,\n 'warn',\n false\n );\n delete treeObject.tree[attribute];\n }\n }\n try {\n await putTree({\n treeId: treeObject.tree._id as string,\n treeData: treeObject.tree,\n state,\n });\n if (verbose) printMessage(`\\n - Done`, 'info', true);\n } catch (importError2) {\n printMessage(importError2.response.data, 'error');\n throw new Error(`Error importing journey flow ${treeId}`);\n }\n } else {\n printMessage(importError.response?.data || importError, 'error');\n debugMessage(importError.response?.data || importError);\n throw new Error(`\\nError importing journey flow ${treeId}`);\n }\n }\n return true;\n}\n\n/**\n * Resolve journey dependencies\n * @param {Map} installedJorneys Map of installed journeys\n * @param {Map} journeyMap Map of journeys to resolve dependencies for\n * @param {[String]} unresolvedJourneys Map to hold the names of unresolved journeys and their dependencies\n * @param {[String]} resolvedJourneys Array to hold the names of resolved journeys\n * @param {int} index Depth of recursion\n */\nexport async function resolveDependencies(\n installedJorneys,\n journeyMap,\n unresolvedJourneys,\n resolvedJourneys,\n index = -1\n) {\n let before = -1;\n let after = index;\n if (index !== -1) {\n before = index;\n }\n\n for (const tree in journeyMap) {\n if ({}.hasOwnProperty.call(journeyMap, tree)) {\n const dependencies = [];\n for (const node in journeyMap[tree].nodes) {\n if (\n journeyMap[tree].nodes[node]._type._id === 'InnerTreeEvaluatorNode'\n ) {\n dependencies.push(journeyMap[tree].nodes[node].tree);\n }\n }\n let allResolved = true;\n for (const dependency of dependencies) {\n if (\n !resolvedJourneys.includes(dependency) &&\n !installedJorneys.includes(dependency)\n ) {\n allResolved = false;\n }\n }\n if (allResolved) {\n if (resolvedJourneys.indexOf(tree) === -1) resolvedJourneys.push(tree);\n // remove from unresolvedJourneys array\n // for (let i = 0; i < unresolvedJourneys.length; i += 1) {\n // if (unresolvedJourneys[i] === tree) {\n // unresolvedJourneys.splice(i, 1);\n // i -= 1;\n // }\n // }\n delete unresolvedJourneys[tree];\n // } else if (!unresolvedJourneys.includes(tree)) {\n } else {\n // unresolvedJourneys.push(tree);\n unresolvedJourneys[tree] = dependencies;\n }\n }\n }\n after = Object.keys(unresolvedJourneys).length;\n if (index !== -1 && after === before) {\n // This is the end, no progress was made since the last recursion\n // printMessage(\n // `Journeys with unresolved dependencies: ${unresolvedJourneys}`,\n // 'error'\n // );\n } else if (after > 0) {\n resolveDependencies(\n installedJorneys,\n journeyMap,\n unresolvedJourneys,\n resolvedJourneys,\n after\n );\n }\n}\n\n/**\n * Helper to import multiple trees from a tree map\n * @param {Object} treesMap map of trees object\n * @param {TreeImportOptions} options import options\n */\nexport async function importAllJourneys({\n treesMap,\n options,\n state,\n}: {\n treesMap: MultiTreeExportInterface;\n options: TreeImportOptions;\n state: State;\n}) {\n const installedJourneys = (await getTrees({ state })).result.map(\n (x) => x._id\n );\n const unresolvedJourneys = {};\n const resolvedJourneys = [];\n createProgressIndicator(undefined, 'Resolving dependencies', 'indeterminate');\n await resolveDependencies(\n installedJourneys,\n treesMap,\n unresolvedJourneys,\n resolvedJourneys\n );\n if (Object.keys(unresolvedJourneys).length === 0) {\n stopProgressIndicator(`Resolved all dependencies.`, 'success');\n } else {\n stopProgressIndicator(\n `${\n Object.keys(unresolvedJourneys).length\n } journeys with unresolved dependencies:`,\n 'fail'\n );\n for (const journey of Object.keys(unresolvedJourneys)) {\n printMessage(\n ` - ${journey} requires ${unresolvedJourneys[journey]}`,\n 'info'\n );\n }\n }\n createProgressIndicator(resolvedJourneys.length, 'Importing');\n for (const tree of resolvedJourneys) {\n try {\n // eslint-disable-next-line no-await-in-loop\n await importJourney({ treeObject: treesMap[tree], options, state });\n updateProgressIndicator(`${tree}`);\n } catch (error) {\n printMessage(`\\n${error.message}`, 'error');\n }\n }\n stopProgressIndicator('Done');\n}\n\n/**\n * Get the node reference obbject for a node object. Node reference objects\n * are used in a tree flow definition and within page nodes to reference\n * nodes. Among other things, node references contain all the non-configuration\n * meta data that exists for readaility, like the x/y coordinates of the node\n * and the display name chosen by the tree designer. The dislay name is the\n * only intuitive link between the graphical representation of the tree and\n * the node configurations that make up the tree.\n * @param nodeObj node object to retrieve the node reference object for\n * @param singleTreeExport tree export with or without dependencies\n * @returns {NodeRefSkeletonInterface | InnerNodeRefSkeletonInterface} node reference object\n */\nexport function getNodeRef(\n nodeObj: NodeSkeleton,\n singleTreeExport: SingleTreeExportInterface\n): NodeRefSkeletonInterface | InnerNodeRefSkeletonInterface {\n if (singleTreeExport.tree.nodes[nodeObj._id]) {\n return singleTreeExport.tree.nodes[nodeObj._id];\n } else {\n for (const node of Object.values(singleTreeExport.nodes)) {\n if (containerNodes.includes(node._type._id)) {\n for (const nodeRef of node.nodes) {\n if (nodeRef._id === nodeObj._id) {\n return nodeRef;\n }\n }\n }\n }\n }\n return undefined;\n}\n\n/**\n * Default tree export resolver used to resolve a tree id/name to a full export\n * w/o dependencies of that tree from a platform instance.\n * @param {string} treeId id/name of the tree to resolve\n * @returns {TreeExportResolverInterface} tree export\n */\nexport const onlineTreeExportResolver: TreeExportResolverInterface =\n async function (treeId: string, state: State) {\n debugMessage(`onlineTreeExportResolver(${treeId})`);\n return await exportJourney({\n treeId,\n options: {\n deps: false,\n useStringArrays: false,\n },\n state,\n });\n };\n\n/**\n * Tree export resolver used to resolve a tree id/name to a full export\n * of that tree from individual `treename.journey.json` export files.\n * @param {string} treeId id/name of the tree to resolve\n * @returns {TreeExportResolverInterface} tree export\n */\nexport const fileByIdTreeExportResolver: TreeExportResolverInterface =\n async function (treeId: string, state: State) {\n debugMessage(`fileByIdTreeExportResolver(${treeId})`);\n let treeExport = createSingleTreeExportTemplate({ state });\n const files = findFilesByName(getTypedFilename(`${treeId}`, 'journey'));\n try {\n const file = files.pop();\n const jsonData = JSON.parse(fs.readFileSync(file, 'utf8'));\n debugMessage(\n `fileByIdTreeExportResolver: resolved '${treeId}' to ${file}`\n );\n // did we resolve the tree we were asked to resolved?\n if (jsonData.tree?._id === treeId) {\n treeExport = jsonData;\n }\n // check if this is a file with multiple trees and get journey by id\n else if (jsonData.trees && jsonData.trees[treeId]) {\n treeExport = jsonData.trees[treeId];\n }\n } catch (error) {\n debugMessage(\n `fileByIdTreeExportResolver: unable to resolve '${treeId}' to a file.`\n );\n }\n return treeExport;\n };\n\n/**\n * Factory that creates a tree export resolver used to resolve a tree id\n * to a full export of that tree from a multi-tree export file.\n * @param {string} file multi-tree export file\n * @returns {TreeExportResolverInterface} tree export resolver\n */\nexport function createFileParamTreeExportResolver(\n file: string,\n state: State\n): TreeExportResolverInterface {\n const fileParamTreeExportResolver: TreeExportResolverInterface =\n async function (treeId: string) {\n debugMessage(`fileParamTreeExportResolver(${treeId})`);\n let treeExport: SingleTreeExportInterface =\n createSingleTreeExportTemplate({ state });\n try {\n const jsonData = JSON.parse(fs.readFileSync(file, 'utf8'));\n // did we resolve the tree we were asked to resolved?\n if (jsonData.tree?._id === treeId) {\n treeExport = jsonData;\n }\n // check if this is a file with multiple trees and get journey by id\n else if (jsonData.trees && jsonData.trees[treeId]) {\n treeExport = jsonData.trees[treeId];\n }\n // fall back to fileByIdTreeExportResolver\n else {\n treeExport = await fileByIdTreeExportResolver(treeId, state);\n }\n } catch (error) {\n //\n }\n return treeExport;\n };\n debugMessage(`fileParamTreeExportResolver: file=${file}`);\n return fileParamTreeExportResolver;\n}\n\n/**\n * Get tree dependencies (all descendent inner trees)\n * @param {SingleTreeExportInterface} treeExport single tree export\n * @param {string[]} resolvedTreeIds list of tree ids wich have already been resolved\n * @param {TreeExportResolverInterface} resolveTreeExport tree export resolver callback function\n * @returns {Promise<TreeDependencyMapInterface>} a promise that resolves to a tree dependency map\n */\nexport async function getTreeDescendents({\n treeExport,\n resolveTreeExport = onlineTreeExportResolver,\n resolvedTreeIds = [],\n state,\n}: {\n treeExport: SingleTreeExportInterface;\n resolveTreeExport: TreeExportResolverInterface;\n resolvedTreeIds: string[];\n state: State;\n}): Promise<TreeDependencyMapInterface> {\n debugMessage(\n `getTreeDependencies(${treeExport.tree._id}, [${resolvedTreeIds.join(\n ', '\n )}])`\n );\n if (!resolvedTreeIds.includes(treeExport.tree._id)) {\n resolvedTreeIds.push(treeExport.tree._id);\n }\n const treeDependencyMap: TreeDependencyMapInterface = {\n [treeExport.tree._id]: [],\n };\n const dependencies: TreeDependencyMapInterface[] = [];\n for (const [nodeId, node] of Object.entries(treeExport.tree.nodes)) {\n const innerTreeId = treeExport.nodes[nodeId].tree;\n if (\n node.nodeType === 'InnerTreeEvaluatorNode' &&\n !resolvedTreeIds.includes(innerTreeId)\n ) {\n const innerTreeExport = await resolveTreeExport(innerTreeId, state);\n debugMessage(`resolved inner tree: ${innerTreeExport.tree._id}`);\n // resolvedTreeIds.push(innerTreeId);\n dependencies.push(\n await getTreeDescendents({\n treeExport: innerTreeExport,\n resolveTreeExport,\n resolvedTreeIds,\n state,\n })\n );\n }\n }\n treeDependencyMap[treeExport.tree._id] = dependencies;\n return treeDependencyMap;\n}\n\n/**\n * Find all node configuration objects that are no longer referenced by any tree\n * @returns {Promise<unknown[]>} a promise that resolves to an array of orphaned nodes\n */\nexport async function findOrphanedNodes({\n state,\n}: {\n state: State;\n}): Promise<NodeSkeleton[]> {\n const allNodes = [];\n const orphanedNodes = [];\n let types = [];\n const allJourneys = (await getTrees({ state })).result;\n let errorMessage = '';\n const errorTypes = [];\n\n createProgressIndicator(\n undefined,\n `Counting total nodes...`,\n 'indeterminate'\n );\n try {\n types = (await getNodeTypes({ state })).result;\n } catch (error) {\n printMessage('Error retrieving all available node types:', 'error');\n printMessage(error.response.data, 'error');\n return [];\n }\n for (const type of types) {\n try {\n // eslint-disable-next-line no-await-in-loop, no-loop-func\n const nodes = (await getNodesByType({ nodeType: type._id, state }))\n .result;\n for (const node of nodes) {\n allNodes.push(node);\n updateProgressIndicator(\n `${allNodes.length} total nodes${errorMessage}`\n );\n }\n } catch (error) {\n errorTypes.push(type._id);\n errorMessage = ` (Skipped type(s): ${errorTypes})`['yellow'];\n updateProgressIndicator(`${allNodes.length} total nodes${errorMessage}`);\n }\n }\n if (errorTypes.length > 0) {\n stopProgressIndicator(\n `${allNodes.length} total nodes${errorMessage}`,\n 'warn'\n );\n } else {\n stopProgressIndicator(`${allNodes.length} total nodes`, 'success');\n }\n\n createProgressIndicator(\n undefined,\n 'Counting active nodes...',\n 'indeterminate'\n );\n const activeNodes = [];\n for (const journey of allJourneys) {\n for (const nodeId in journey.nodes) {\n if ({}.hasOwnProperty.call(journey.nodes, nodeId)) {\n activeNodes.push(nodeId);\n updateProgressIndicator(`${activeNodes.length} active nodes`);\n const node = journey.nodes[nodeId];\n if (containerNodes.includes(node.nodeType)) {\n const containerNode = await getNode({\n nodeId,\n nodeType: node.nodeType,\n state,\n });\n for (const innerNode of containerNode.nodes) {\n activeNodes.push(innerNode._id);\n updateProgressIndicator(`${activeNodes.length} active nodes`);\n }\n }\n }\n }\n }\n stopProgressIndicator(`${activeNodes.length} active nodes`, 'success');\n\n createProgressIndicator(\n undefined,\n 'Calculating orphaned nodes...',\n 'indeterminate'\n );\n const diff = allNodes.filter((x) => !activeNodes.includes(x._id));\n for (const orphanedNode of diff) {\n orphanedNodes.push(orphanedNode);\n }\n stopProgressIndicator(`${orphanedNodes.length} orphaned nodes`, 'success');\n return orphanedNodes;\n}\n\n/**\n * Remove orphaned nodes\n * @param {NodeSkeleton[]} orphanedNodes Pass in an array of orphaned node configuration objects to remove\n * @returns {Promise<NodeSkeleton[]>} a promise that resolves to an array nodes that encountered errors deleting\n */\nexport async function removeOrphanedNodes({\n orphanedNodes,\n state,\n}: {\n orphanedNodes: NodeSkeleton[];\n state: State;\n}): Promise<NodeSkeleton[]> {\n const errorNodes = [];\n createProgressIndicator(orphanedNodes.length, 'Removing orphaned nodes...');\n for (const node of orphanedNodes) {\n updateProgressIndicator(`Removing ${node['_id']}...`);\n try {\n // eslint-disable-next-line no-await-in-loop\n await deleteNode({\n nodeId: node['_id'],\n nodeType: node['_type']['_id'],\n state,\n });\n } catch (deleteError) {\n errorNodes.push(node);\n printMessage(` ${deleteError}`, 'error');\n }\n }\n stopProgressIndicator(`Removed ${orphanedNodes.length} orphaned nodes.`);\n return errorNodes;\n}\n\n/**\n * Analyze if a journey contains any custom nodes considering the detected or the overridden version.\n * @param {SingleTreeExportInterface} journey Journey/tree configuration object\n * @returns {boolean} True if the journey/tree contains any custom nodes, false otherwise.\n */\nexport function isCustomJourney({\n journey,\n state,\n}: {\n journey: SingleTreeExportInterface;\n state: State;\n}) {\n const nodeList = Object.values(journey.nodes).concat(\n Object.values(journey.innerNodes)\n );\n for (const node of nodeList) {\n if (isCustomNode({ nodeType: node['_type']['_id'], state })) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Analyze if a journey contains any premium nodes considering the detected or the overridden version.\n * @param {SingleTreeExportInterface} journey Journey/tree configuration object\n * @returns {boolean} True if the journey/tree contains any custom nodes, false otherwise.\n */\nexport function isPremiumJourney(journey: SingleTreeExportInterface) {\n const nodeList = Object.values(journey.nodes).concat(\n Object.values(journey.innerNodes)\n );\n for (const node of nodeList) {\n if (isPremiumNode(node['_type']['_id'])) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Analyze if a journey contains any cloud-only nodes considering the detected or the overridden version.\n * @param {SingleTreeExportInterface} journey Journey/tree configuration object\n * @returns {boolean} True if the journey/tree contains any cloud-only nodes, false otherwise.\n */\nexport function isCloudOnlyJourney(journey: SingleTreeExportInterface) {\n const nodeList = Object.values(journey.nodes).concat(\n Object.values(journey.innerNodes)\n );\n for (const node of nodeList) {\n if (isCloudOnlyNode(node['_type']['_id'])) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Get a journey's classifications, which can be one or multiple of:\n * - standard: can run on any instance of a ForgeRock platform\n * - cloud: utilize nodes, which are exclusively available in the ForgeRock Identity Cloud\n * - premium: utilizes nodes, which come at a premium\n * - custom: utilizes nodes not included in the ForgeRock platform release\n * @param {SingleTreeExportInterface} journey journey export data\n * @returns {JourneyClassification[]} an array of one or multiple classifications\n */\nexport function getJourneyClassification({\n journey,\n state,\n}: {\n journey: SingleTreeExportInterface;\n state: State;\n}): JourneyClassification[] {\n const classifications: JourneyClassification[] = [];\n const premium = isPremiumJourney(journey);\n const custom = isCustomJourney({ journey, state });\n const cloud = isCloudOnlyJourney(journey);\n if (custom) {\n classifications.push(JourneyClassification.CUSTOM);\n } else if (cloud) {\n classifications.push(JourneyClassification.CLOUD);\n } else {\n classifications.push(JourneyClassification.STANDARD);\n }\n if (premium) classifications.push(JourneyClassification.PREMIUM);\n return classifications;\n}\n\n/**\n * Delete a journey\n * @param {string} journeyId journey id/name\n * @param {Object} options deep=true also delete all the nodes and inner nodes, verbose=true print verbose info\n */\nexport async function deleteJourney({\n journeyId,\n options,\n state,\n}: {\n journeyId: string;\n options: { deep: boolean; verbose: boolean; progress?: boolean };\n state: State;\n}) {\n const { deep, verbose } = options;\n const progress = !('progress' in options) ? true : options.progress;\n const status = { nodes: {} };\n if (progress)\n createProgressIndicator(\n undefined,\n `Deleting ${journeyId}...`,\n 'indeterminate'\n );\n if (progress && verbose) stopProgressIndicator();\n return deleteTree({ treeId: journeyId, state })\n .then(async (deleteTreeResponse) => {\n status['status'] = 'success';\n const nodePromises = [];\n if (verbose) printMessage(`Deleted ${journeyId} (tree)`, 'info');\n if (deep) {\n for (const [nodeId, nodeObject] of Object.entries(\n deleteTreeResponse.nodes\n )) {\n // delete inner nodes (nodes inside container nodes)\n if (containerNodes.includes(nodeObject['nodeType'])) {\n try {\n // eslint-disable-next-line no-await-in-loop\n const containerNode = await getNode({\n nodeId,\n nodeType: nodeObject['nodeType'],\n state,\n });\n if (verbose)\n printMessage(\n `Read ${nodeId} (${nodeObject['nodeType']}) from ${journeyId}`,\n 'info'\n );\n for (const innerNodeObject of containerNode.nodes) {\n nodePromises.push(\n deleteNode({\n nodeId: innerNodeObject._id,\n nodeType: innerNodeObject.nodeType,\n state,\n })\n .then((response2) => {\n status.nodes[innerNodeObject._id] = { status: 'success' };\n if (verbose)\n printMessage(\n `Deleted ${innerNodeObject._id} (${innerNodeObject.nodeType}) from ${journeyId}`,\n 'info'\n );\n return response2;\n })\n .catch((error) => {\n status.nodes[innerNodeObject._id] = {\n status: 'error',\n error,\n };\n if (verbose)\n printMessage(\n `Error deleting inner node ${innerNodeObject._id} (${innerNodeObject.nodeType}) from ${journeyId}: ${error}`,\n 'error'\n );\n })\n );\n }\n // finally delete the container node\n nodePromises.push(\n deleteNode({\n nodeId: containerNode._id,\n nodeType: containerNode['_type']['_id'],\n state,\n })\n .then((response2) => {\n status.nodes[containerNode._id] = { status: 'success' };\n if (verbose)\n printMessage(\n `Deleted ${containerNode._id} (${containerNode['_type']['_id']}) from ${journeyId}`,\n 'info'\n );\n return response2;\n })\n .catch((error) => {\n if (\n error?.response?.data?.code === 500 &&\n error.response.data.message ===\n 'Unable to read SMS config: Node did not exist'\n ) {\n status.nodes[containerNode._id] = { status: 'success' };\n if (verbose)\n printMessage(\n `Deleted ${containerNode._id} (${containerNode['_type']['_id']}) from ${journeyId}`,\n 'info'\n );\n } else {\n status.nodes[containerNode._id] = {\n status: 'error',\n error,\n };\n if (verbose)\n printMessage(\n `Error deleting container node ${containerNode._id} (${containerNode['_type']['_id']}) from ${journeyId}: ${error.response.data.message}`,\n 'error'\n );\n }\n })\n );\n } catch (error) {\n if (verbose)\n printMessage(\n `Error getting container node ${nodeId} (${nodeObject['nodeType']}) from ${journeyId}: ${error}`,\n 'error'\n );\n }\n } else {\n // delete the node\n nodePromises.push(\n deleteNode({ nodeId, nodeType: nodeObject['nodeType'], state })\n .then((response) => {\n status.nodes[nodeId] = { status: 'success' };\n if (verbose)\n printMessage(\n `Deleted ${nodeId} (${nodeObject['nodeType']}) from ${journeyId}`,\n 'info'\n );\n return response;\n })\n .catch((error) => {\n status.nodes[nodeId] = { status: 'error', error };\n if (verbose)\n printMessage(\n `Error deleting node ${nodeId} (${nodeObject['nodeType']}) from ${journeyId}: ${error}`,\n 'error'\n );\n })\n );\n }\n }\n }\n // wait until all the node calls are complete\n await Promise.allSettled(nodePromises);\n\n // report status\n if (progress) {\n let nodeCount = 0;\n let errorCount = 0;\n for (const node of Object.keys(status.nodes)) {\n nodeCount += 1;\n if (status.nodes[node].status === 'error') errorCount += 1;\n }\n if (errorCount === 0) {\n stopProgressIndicator(\n `Deleted ${journeyId} and ${\n nodeCount - errorCount\n }/${nodeCount} nodes.`,\n 'success'\n );\n } else {\n stopProgressIndicator(\n `Deleted ${journeyId} and ${\n nodeCount - errorCount\n }/${nodeCount} nodes.`,\n 'fail'\n );\n }\n }\n return status;\n })\n .catch((error) => {\n status['status'] = 'error';\n status['error'] = error;\n stopProgressIndicator(`Error deleting ${journeyId}.`, 'fail');\n if (verbose)\n printMessage(`Error deleting tree ${journeyId}: ${error}`, 'error');\n return status;\n });\n}\n\n/**\n * Delete all journeys\n * @param {Object} options deep=true also delete all the nodes and inner nodes, verbose=true print verbose info\n */\nexport async function deleteJourneys({\n options,\n state,\n}: {\n options?: {\n deep: boolean;\n verbose: boolean;\n };\n state: State;\n}) {\n const { verbose } = options;\n const status = {};\n const trees = (await getTrees({ state })).result;\n createProgressIndicator(trees.length, 'Deleting journeys...');\n for (const tree of trees) {\n if (verbose) printMessage('');\n options['progress'] = false;\n status[tree._id] = await deleteJourney({\n journeyId: tree._id,\n options,\n state,\n });\n updateProgressIndicator(`${tree._id}`);\n // introduce a 100ms wait to allow the progress bar to update before the next verbose message prints from the async function\n if (verbose)\n // eslint-disable-next-line no-await-in-loop\n await new Promise((r) => {\n setTimeout(r, 100);\n });\n }\n let journeyCount = 0;\n let journeyErrorCount = 0;\n let nodeCount = 0;\n let nodeErrorCount = 0;\n for (const journey of Object.keys(status)) {\n journeyCount += 1;\n if (status[journey].status === 'error') journeyErrorCount += 1;\n for (const node of Object.keys(status[journey].nodes)) {\n nodeCount += 1;\n if (status[journey].nodes[node].status === 'error') nodeErrorCount += 1;\n }\n }\n stopProgressIndicator(\n `Deleted ${journeyCount - journeyErrorCount}/${journeyCount} journeys and ${\n nodeCount - nodeErrorCount\n }/${nodeCount} nodes.`\n );\n return status;\n}\n\n/**\n * Enable a journey\n * @param journeyId journey id/name\n * @returns {Promise<boolean>} true if the operation was successful, false otherwise\n */\nexport async function enableJourney({\n journeyId,\n state,\n}: {\n journeyId: string;\n state: State;\n}): Promise<boolean> {\n try {\n const treeObject = await getTree({ id: journeyId, state });\n treeObject['enabled'] = true;\n delete treeObject._rev;\n const newTreeObject = await putTree({\n treeId: journeyId,\n treeData: treeObject,\n state,\n });\n return newTreeObject['enabled'] === true;\n } catch (error) {\n printMessage(error.response.data, 'error');\n return false;\n }\n}\n\n/**\n * Disable a journey\n * @param journeyId journey id/name\n * @returns {Promise<boolean>} true if the operation was successful, false otherwise\n */\nexport async function disableJourney({\n journeyId,\n state,\n}: {\n journeyId: string;\n state: State;\n}): Promise<boolean> {\n try {\n const treeObject = await getTree({ id: journeyId, state });\n treeObject['enabled'] = false;\n delete treeObject._rev;\n const newTreeObject = await putTree({\n treeId: journeyId,\n treeData: treeObject,\n state,\n });\n return newTreeObject['enabled'] === false;\n } catch (error) {\n printMessage(error.response.data, 'error');\n return false;\n }\n}\n"]}
@@ -0,0 +1,11 @@
1
+ import State from '../shared/State';
2
+ import { AxiosRequestConfig } from 'axios';
3
+ export { accessToken, authorize, clientCredentialsGrant, getTokenInfo, } from '../api/OAuth2OIDCApi';
4
+ export default class OAuth2OidcOps {
5
+ state: State;
6
+ constructor(state: State);
7
+ authorize(amBaseUrl: string, data: string, config: AxiosRequestConfig): Promise<import("axios").AxiosResponse<any, any>>;
8
+ accessToken(amBaseUrl: string, data: any, config: AxiosRequestConfig): Promise<import("axios").AxiosResponse<any, any>>;
9
+ getTokenInfo(amBaseUrl: string, config: AxiosRequestConfig): Promise<any>;
10
+ clientCredentialsGrant(amBaseUrl: string, clientId: string, clientSecret: string, scope: string): Promise<any>;
11
+ }