From 09ace550ef3809d25b20f3447591ac1d00a9d802 Mon Sep 17 00:00:00 2001 From: harshitha-cstk Date: Tue, 25 Aug 2026 14:01:13 +0530 Subject: [PATCH 1/5] fix(import): remap nested entry-data extension UIDs for is_asset custom fields Marketplace-app / extension references embedded in entry DATA (e.g. an `image_presets` value's `metadata.extension_uid`) are stack-scoped and must be remapped to the destination app's extension_uid during import. The remap in `lookupAssets` (`findAssetIdsFromJsonCustomFields`) only walked top-level schema fields, so any `is_asset` JSON custom field nested inside a group / global_field / blocks was skipped. Consequences of the miss: - the entry kept the source app's extension_uid, leaving an orphaned reference on the destination stack; and - on a subsequent import the audit flagged that UID as a missing reference and stripped the whole field (silent data loss of image-preset configs). Replace the flat `ctSchema.map` with a recursive walk (`remapJsonCustomFieldExtensionUids`) that follows the entry-data shape through group / global_field / blocks and handles multiple-valued fields, remapping `metadata.extension_uid` (and the schema field's extension_uid) via the marketplace_apps mapping. Co-Authored-By: Claude Opus 4.8 --- .../src/utils/asset-helper.ts | 65 +++++++++++++++---- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/packages/contentstack-import/src/utils/asset-helper.ts b/packages/contentstack-import/src/utils/asset-helper.ts index e1110656e..f47e2f0d5 100644 --- a/packages/contentstack-import/src/utils/asset-helper.ts +++ b/packages/contentstack-import/src/utils/asset-helper.ts @@ -149,25 +149,62 @@ export const lookupAssets = function ( function findAssetIdsFromJsonCustomFields(entryObj: any, ctSchema: any) { log.debug('Processing JSON custom fields for asset references'); - ctSchema.map((row: any) => { - if (row.data_type === 'json') { - if (entryObj[row.uid] && row.field_metadata.extension && row.field_metadata.is_asset) { - if (installedExtensions && installedExtensions[row.extension_uid]) { - log.debug(`Mapping extension UID in custom field: ${row.extension_uid}`); - row.extension_uid = installedExtensions[row.extension_uid]; - } + // NOTE: image-preset (and other is_asset custom-field) values carry a stack-scoped + // `metadata.extension_uid` inside the entry DATA. This must be remapped to the destination + // app's extension_uid, otherwise the reference is orphaned in the destination (and a later + // audit strips it as a missing reference -> silent data loss). These fields can be nested + // inside group / global_field / blocks, so walk the schema and entry data recursively rather + // than only the top level. + remapJsonCustomFieldExtensionUids(ctSchema, entryObj); + } - if (entryObj[row.uid].metadata && entryObj[row.uid].metadata.extension_uid) { - if (installedExtensions && installedExtensions[entryObj[row.uid].metadata.extension_uid]) { - log.debug(`Mapping metadata extension UID: ${entryObj[row.uid].metadata.extension_uid}`); - entryObj[row.uid].metadata.extension_uid = installedExtensions[entryObj[row.uid].metadata.extension_uid]; + // Recursively remap `metadata.extension_uid` (and the schema field's extension_uid) for + // is_asset JSON custom fields, following the entry-data shape through group / global_field / + // blocks and multiple-valued fields. + function remapJsonCustomFieldExtensionUids(schema: any[], dataNode: any) { + if (!Array.isArray(schema) || !dataNode || typeof dataNode !== 'object') return; + // A `multiple: true` group/field is stored as an array of nodes. + const dataNodes = Array.isArray(dataNode) ? dataNode : [dataNode]; + + for (const node of dataNodes) { + if (!node || typeof node !== 'object') continue; + + for (const field of schema) { + const { uid, data_type } = field || {}; + if (!uid) continue; + const value = node[uid]; + if (value === undefined || value === null) continue; + + if (data_type === 'json' && field.field_metadata?.extension && field.field_metadata?.is_asset) { + // Remap the schema field's extension_uid (parity with previous behavior). + if (installedExtensions && installedExtensions[field.extension_uid]) { + log.debug(`Mapping extension UID in custom field: ${field.extension_uid}`); + field.extension_uid = installedExtensions[field.extension_uid]; + } + // Remap the entry-data metadata.extension_uid for each value (single or multiple). + const values = Array.isArray(value) ? value : [value]; + for (const val of values) { + const currentUid = val?.metadata?.extension_uid; + if (currentUid && installedExtensions && installedExtensions[currentUid]) { + log.debug(`Mapping metadata extension UID: ${currentUid} -> ${installedExtensions[currentUid]}`); + val.metadata.extension_uid = installedExtensions[currentUid]; + } + } + } else if (data_type === 'group' || data_type === 'global_field') { + remapJsonCustomFieldExtensionUids(field.schema, value); + } else if (data_type === 'blocks' && Array.isArray(field.blocks)) { + const blockInstances = Array.isArray(value) ? value : [value]; + for (const blockInstance of blockInstances) { + if (!blockInstance || typeof blockInstance !== 'object') continue; + for (const blockDef of field.blocks) { + if (blockDef?.uid && blockInstance[blockDef.uid] && blockDef.schema) { + remapJsonCustomFieldExtensionUids(blockDef.schema, blockInstance[blockDef.uid]); + } } } } } - - return row; - }); + } } function findAssetIdsFromHtmlRte(entryObj: any, ctSchema: any) { From 555a73fcb263aeaff3f8eeaf7a53927ffb419a10 Mon Sep 17 00:00:00 2001 From: harshitha-cstk Date: Wed, 26 Aug 2026 11:40:55 +0530 Subject: [PATCH 2/5] fix(import): survive marketplace app config decrypt failure without aborting the module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `updateAppsConfig` called `nodeCrypto.decrypt(configuration)` inline as the argument to `.setConfiguration(...)` / `.setServerConfig(...)`. A bad-decrypt (e.g. ERR_OSSL_BAD_DECRYPT when an app's exported `configuration` was encrypted with a different key) therefore threw synchronously — outside the promise chain's `.catch` — and propagated up through `installApps` (which re-throws), aborting the entire marketplace-apps module. Because the module aborted, `mapper/marketplace_apps/uid-mapping.json` was never written. Global fields / content types (and entry data) then had no marketplace extension mapping to remap against, so their imports failed with "extension_uid ... does not exist in destination stack". Fix: decrypt inside a try/catch for both configuration and server_configuration. On failure, log a warning and skip only that app's config push; the app stays installed and its extension_uid mapping is still recorded, so downstream GF/CT/entry remapping works and the uid-mapping file is written. Co-Authored-By: Claude Opus 4.8 --- .../src/import/modules/marketplace-apps.ts | 110 ++++++++++++------ 1 file changed, 74 insertions(+), 36 deletions(-) diff --git a/packages/contentstack-import/src/import/modules/marketplace-apps.ts b/packages/contentstack-import/src/import/modules/marketplace-apps.ts index cb150583a..e418a4256 100644 --- a/packages/contentstack-import/src/import/modules/marketplace-apps.ts +++ b/packages/contentstack-import/src/import/modules/marketplace-apps.ts @@ -697,46 +697,84 @@ export default class ImportMarketplaceApps extends BaseClass { if (!isEmpty(configuration)) { log.debug(`Updating app configuration for: ${appName}`, this.importConfig.context); - await this.appSdk - .marketplace(this.importConfig.org_uid) - .installation(installation_uid) - .setConfiguration(this.nodeCrypto.decrypt(configuration)) - .then(({ data }: any) => { - if (data?.message) { - log.debug(data, this.importConfig.context); - log.info(formatError(data.message), this.importConfig.context); - } else { - log.success(`${appName} app config updated successfully.!`, this.importConfig.context); - log.debug(`Configuration update successful for: ${appName}`, this.importConfig.context); - } - }) - .catch((error: any) => { - log.debug(error, this.importConfig.context); - log.error(formatError(error), this.importConfig.context); - log.debug(`Configuration update failed for: ${appName}`, this.importConfig.context); - }); + // NOTE: decrypt synchronously in a guard. A bad-decrypt (e.g. ERR_OSSL_BAD_DECRYPT when the + // export was encrypted with a different key) would otherwise throw here — outside the promise + // chain's .catch — abort the whole marketplace-apps module, and skip writing the + // marketplace_apps uid-mapping, which starves the downstream GF/CT/entry extension remap. + // Instead: warn and skip only this app's configuration; the app stays installed and its + // extension mappings are still recorded. + let decryptedConfiguration: any; + try { + decryptedConfiguration = this.nodeCrypto.decrypt(configuration); + } catch (error: any) { + log.warn( + `Failed to decrypt configuration for '${appName}'; skipping its configuration update. The app is installed and its extension mappings are preserved. (${ + error?.message || error + })`, + this.importConfig.context, + ); + decryptedConfiguration = undefined; + } + + if (decryptedConfiguration !== undefined) { + await this.appSdk + .marketplace(this.importConfig.org_uid) + .installation(installation_uid) + .setConfiguration(decryptedConfiguration) + .then(({ data }: any) => { + if (data?.message) { + log.debug(data, this.importConfig.context); + log.info(formatError(data.message), this.importConfig.context); + } else { + log.success(`${appName} app config updated successfully.!`, this.importConfig.context); + log.debug(`Configuration update successful for: ${appName}`, this.importConfig.context); + } + }) + .catch((error: any) => { + log.debug(error, this.importConfig.context); + log.error(formatError(error), this.importConfig.context); + log.debug(`Configuration update failed for: ${appName}`, this.importConfig.context); + }); + } } if (!isEmpty(server_configuration)) { log.debug(`Updating server configuration for: ${appName}`, this.importConfig.context); - await this.appSdk - .marketplace(this.importConfig.org_uid) - .installation(installation_uid) - .setServerConfig(this.nodeCrypto.decrypt(server_configuration)) - .then(({ data }: any) => { - if (data?.message) { - log.debug(data, this.importConfig.context); - log.error(formatError(data.message), this.importConfig.context); - } else { - log.success(`${appName} app server config updated successfully.!`, this.importConfig.context); - log.debug(`Server configuration update successful for: ${appName}`, this.importConfig.context); - } - }) - .catch((error: any) => { - log.debug(error, this.importConfig.context); - log.error(formatError(error), this.importConfig.context); - log.debug(`Server configuration update failed for: ${appName}`, this.importConfig.context); - }); + // NOTE: guard the decrypt for the same reason as `configuration` above — a bad-decrypt must + // not abort the module or skip the uid-mapping write. + let decryptedServerConfiguration: any; + try { + decryptedServerConfiguration = this.nodeCrypto.decrypt(server_configuration); + } catch (error: any) { + log.warn( + `Failed to decrypt server configuration for '${appName}'; skipping its server configuration update. The app is installed and its extension mappings are preserved. (${ + error?.message || error + })`, + this.importConfig.context, + ); + decryptedServerConfiguration = undefined; + } + + if (decryptedServerConfiguration !== undefined) { + await this.appSdk + .marketplace(this.importConfig.org_uid) + .installation(installation_uid) + .setServerConfig(decryptedServerConfiguration) + .then(({ data }: any) => { + if (data?.message) { + log.debug(data, this.importConfig.context); + log.error(formatError(data.message), this.importConfig.context); + } else { + log.success(`${appName} app server config updated successfully.!`, this.importConfig.context); + log.debug(`Server configuration update successful for: ${appName}`, this.importConfig.context); + } + }) + .catch((error: any) => { + log.debug(error, this.importConfig.context); + log.error(formatError(error), this.importConfig.context); + log.debug(`Server configuration update failed for: ${appName}`, this.importConfig.context); + }); + } } } From 191aaa3726dc6d86ae42093f0a8e1d7125de631e Mon Sep 17 00:00:00 2001 From: harshitha-cstk Date: Wed, 26 Aug 2026 12:14:18 +0530 Subject: [PATCH 3/5] refactor(import): make entry-data extension_uid remap schema-independent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR review on the entry-data remap: - Replace the schema-driven walker with `remapEntryMetadataExtensionUids`, a schema-independent deep walk of the entry data that remaps any `metadata.extension_uid` via the marketplace_apps mapping. This is robust to is_asset custom fields nested in group / global_field / blocks AND to content-type schemas that carry a reference-only global_field stub (no expanded `schema`, e.g. from query-export), which the previous schema-driven walk would silently skip. - Run it once, unconditionally, after `find()` (idempotent — already-mapped UIDs are no-ops). - Guard `find()` against a missing schema so a global_field stub no longer throws while collecting assets. - Keep the schema field `extension_uid` remap for parity. Co-Authored-By: Claude Opus 4.8 --- .../src/utils/asset-helper.ts | 102 +++++++++--------- 1 file changed, 49 insertions(+), 53 deletions(-) diff --git a/packages/contentstack-import/src/utils/asset-helper.ts b/packages/contentstack-import/src/utils/asset-helper.ts index f47e2f0d5..9fc678f00 100644 --- a/packages/contentstack-import/src/utils/asset-helper.ts +++ b/packages/contentstack-import/src/utils/asset-helper.ts @@ -93,6 +93,10 @@ export const lookupAssets = function ( let matchedUrls: string[] = []; let find = function (schema: any, entryToFind: any) { + // Guard against a missing schema — e.g. a reference-only global_field stub + // (data_type: 'global_field', reference_to, no expanded `schema`) as produced by + // query-export. Without this, recursing into an undefined schema throws. + if (!Array.isArray(schema)) return; for (let i = 0, _i = schema.length; i < _i; i++) { if ( schema[i].data_type === 'text' && @@ -149,61 +153,48 @@ export const lookupAssets = function ( function findAssetIdsFromJsonCustomFields(entryObj: any, ctSchema: any) { log.debug('Processing JSON custom fields for asset references'); - // NOTE: image-preset (and other is_asset custom-field) values carry a stack-scoped - // `metadata.extension_uid` inside the entry DATA. This must be remapped to the destination - // app's extension_uid, otherwise the reference is orphaned in the destination (and a later - // audit strips it as a missing reference -> silent data loss). These fields can be nested - // inside group / global_field / blocks, so walk the schema and entry data recursively rather - // than only the top level. - remapJsonCustomFieldExtensionUids(ctSchema, entryObj); + // Parity with previous behavior: remap the schema field's extension_uid for is_asset + // JSON custom fields present at this schema level. (Entry-data remap is handled + // schema-independently by remapEntryMetadataExtensionUids — see below.) + if (!Array.isArray(ctSchema)) return; + for (const row of ctSchema) { + if ( + row?.data_type === 'json' && + row?.field_metadata?.extension && + row?.field_metadata?.is_asset && + entryObj?.[row.uid] && + installedExtensions && + installedExtensions[row.extension_uid] + ) { + log.debug(`Mapping extension UID in custom field: ${row.extension_uid}`); + row.extension_uid = installedExtensions[row.extension_uid]; + } + } } - // Recursively remap `metadata.extension_uid` (and the schema field's extension_uid) for - // is_asset JSON custom fields, following the entry-data shape through group / global_field / - // blocks and multiple-valued fields. - function remapJsonCustomFieldExtensionUids(schema: any[], dataNode: any) { - if (!Array.isArray(schema) || !dataNode || typeof dataNode !== 'object') return; - // A `multiple: true` group/field is stored as an array of nodes. - const dataNodes = Array.isArray(dataNode) ? dataNode : [dataNode]; - - for (const node of dataNodes) { - if (!node || typeof node !== 'object') continue; - - for (const field of schema) { - const { uid, data_type } = field || {}; - if (!uid) continue; - const value = node[uid]; - if (value === undefined || value === null) continue; - - if (data_type === 'json' && field.field_metadata?.extension && field.field_metadata?.is_asset) { - // Remap the schema field's extension_uid (parity with previous behavior). - if (installedExtensions && installedExtensions[field.extension_uid]) { - log.debug(`Mapping extension UID in custom field: ${field.extension_uid}`); - field.extension_uid = installedExtensions[field.extension_uid]; - } - // Remap the entry-data metadata.extension_uid for each value (single or multiple). - const values = Array.isArray(value) ? value : [value]; - for (const val of values) { - const currentUid = val?.metadata?.extension_uid; - if (currentUid && installedExtensions && installedExtensions[currentUid]) { - log.debug(`Mapping metadata extension UID: ${currentUid} -> ${installedExtensions[currentUid]}`); - val.metadata.extension_uid = installedExtensions[currentUid]; - } - } - } else if (data_type === 'group' || data_type === 'global_field') { - remapJsonCustomFieldExtensionUids(field.schema, value); - } else if (data_type === 'blocks' && Array.isArray(field.blocks)) { - const blockInstances = Array.isArray(value) ? value : [value]; - for (const blockInstance of blockInstances) { - if (!blockInstance || typeof blockInstance !== 'object') continue; - for (const blockDef of field.blocks) { - if (blockDef?.uid && blockInstance[blockDef.uid] && blockDef.schema) { - remapJsonCustomFieldExtensionUids(blockDef.schema, blockInstance[blockDef.uid]); - } - } - } - } - } + // Remap `metadata.extension_uid` anywhere in the entry DATA to the destination app's + // extension_uid via the marketplace_apps mapping (`installedExtensions`). + // + // is_asset custom-field values (e.g. `image_presets`) carry a stack-scoped + // `metadata.extension_uid` in the entry data. Left unmapped, the reference is orphaned on + // the destination and a later audit strips it as a missing reference (silent data loss). + // These fields can be nested inside group / global_field / blocks, and an imported + // content-type schema may represent a global field as a reference-only stub (no expanded + // `schema`) — so this walks the entry DATA directly instead of relying on the schema shape. + // Only known source->destination app UIDs are remapped, so unrelated values are untouched. + function remapEntryMetadataExtensionUids(node: any) { + if (!node || typeof node !== 'object') return; + if (Array.isArray(node)) { + for (const item of node) remapEntryMetadataExtensionUids(item); + return; + } + const currentUid = node.metadata?.extension_uid; + if (currentUid && installedExtensions && installedExtensions[currentUid]) { + log.debug(`Mapping metadata extension UID: ${currentUid} -> ${installedExtensions[currentUid]}`); + node.metadata.extension_uid = installedExtensions[currentUid]; + } + for (const key of Object.keys(node)) { + remapEntryMetadataExtensionUids(node[key]); } } @@ -303,6 +294,11 @@ export const lookupAssets = function ( } find(data.content_type.schema, data.entry); + // Remap marketplace-app extension UIDs in entry data (e.g. image_presets' metadata.extension_uid) + // once, unconditionally. find() only reaches the is_asset branch when it can descend the schema, + // so a reference-only global_field stub (no expanded schema) would otherwise be missed. This pass + // is schema-independent and idempotent (already-mapped UIDs are no-ops on re-run). + remapEntryMetadataExtensionUids(data.entry); // findFileUrls scans the whole entry object, but is only triggered inside find() when a // text field has markdown/rich_text_type metadata. Content types with no such fields // (e.g. those storing asset URLs in plain text fields) never call findFileUrls, so URLs From 6f53b4e6c79c40d7e85ffd1df20262ffb21bbaa5 Mon Sep 17 00:00:00 2001 From: harshitha-cstk Date: Wed, 26 Aug 2026 12:26:05 +0530 Subject: [PATCH 4/5] refactor(import): make entry-data extension_uid remap schema-independent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR review on the entry-data remap: - Replace the schema-driven walker with `remapEntryMetadataExtensionUids`, a schema-independent deep walk of the entry data that remaps any `metadata.extension_uid` via the marketplace_apps mapping. This is robust to is_asset custom fields nested in group / global_field / blocks AND to content-type schemas that carry a reference-only global_field stub (no expanded `schema`, e.g. from query-export), which the previous schema-driven walk would silently skip. - Run it once, unconditionally, after `find()` (idempotent — already-mapped UIDs are no-ops). - Guard `find()` against a missing schema so a global_field stub no longer throws while collecting assets. - Keep the schema field `extension_uid` remap for parity. Co-Authored-By: Claude Opus 4.8 --- .../src/utils/asset-helper.ts | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/contentstack-import/src/utils/asset-helper.ts b/packages/contentstack-import/src/utils/asset-helper.ts index 9fc678f00..99ca238b2 100644 --- a/packages/contentstack-import/src/utils/asset-helper.ts +++ b/packages/contentstack-import/src/utils/asset-helper.ts @@ -153,23 +153,25 @@ export const lookupAssets = function ( function findAssetIdsFromJsonCustomFields(entryObj: any, ctSchema: any) { log.debug('Processing JSON custom fields for asset references'); - // Parity with previous behavior: remap the schema field's extension_uid for is_asset - // JSON custom fields present at this schema level. (Entry-data remap is handled - // schema-independently by remapEntryMetadataExtensionUids — see below.) - if (!Array.isArray(ctSchema)) return; - for (const row of ctSchema) { - if ( - row?.data_type === 'json' && - row?.field_metadata?.extension && - row?.field_metadata?.is_asset && - entryObj?.[row.uid] && - installedExtensions && - installedExtensions[row.extension_uid] - ) { - log.debug(`Mapping extension UID in custom field: ${row.extension_uid}`); - row.extension_uid = installedExtensions[row.extension_uid]; + ctSchema.map((row: any) => { + if (row.data_type === 'json') { + if (entryObj[row.uid] && row.field_metadata.extension && row.field_metadata.is_asset) { + if (installedExtensions && installedExtensions[row.extension_uid]) { + log.debug(`Mapping extension UID in custom field: ${row.extension_uid}`); + row.extension_uid = installedExtensions[row.extension_uid]; + } + + if (entryObj[row.uid].metadata && entryObj[row.uid].metadata.extension_uid) { + if (installedExtensions && installedExtensions[entryObj[row.uid].metadata.extension_uid]) { + log.debug(`Mapping metadata extension UID: ${entryObj[row.uid].metadata.extension_uid}`); + entryObj[row.uid].metadata.extension_uid = installedExtensions[entryObj[row.uid].metadata.extension_uid]; + } + } + } } - } + + return row; + }); } // Remap `metadata.extension_uid` anywhere in the entry DATA to the destination app's From 9d75f00d940f3f5d5b47da59ba8af8928dc8cdec Mon Sep 17 00:00:00 2001 From: harshitha-cstk Date: Fri, 28 Aug 2026 12:10:09 +0530 Subject: [PATCH 5/5] refactor(import): simplify app config decrypt guard in updateAppsConfig Co-Authored-By: Claude Opus 4.8 --- .../src/import/modules/marketplace-apps.ts | 54 ++++++++----------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/packages/contentstack-import/src/import/modules/marketplace-apps.ts b/packages/contentstack-import/src/import/modules/marketplace-apps.ts index e418a4256..7aa3152cf 100644 --- a/packages/contentstack-import/src/import/modules/marketplace-apps.ts +++ b/packages/contentstack-import/src/import/modules/marketplace-apps.ts @@ -697,26 +697,14 @@ export default class ImportMarketplaceApps extends BaseClass { if (!isEmpty(configuration)) { log.debug(`Updating app configuration for: ${appName}`, this.importConfig.context); - // NOTE: decrypt synchronously in a guard. A bad-decrypt (e.g. ERR_OSSL_BAD_DECRYPT when the - // export was encrypted with a different key) would otherwise throw here — outside the promise - // chain's .catch — abort the whole marketplace-apps module, and skip writing the - // marketplace_apps uid-mapping, which starves the downstream GF/CT/entry extension remap. - // Instead: warn and skip only this app's configuration; the app stays installed and its - // extension mappings are still recorded. - let decryptedConfiguration: any; + // NOTE: decrypt inside the try. A bad-decrypt (e.g. ERR_OSSL_BAD_DECRYPT when the export was + // encrypted with a different key) would otherwise throw — outside the promise chain's .catch — + // and abort the whole marketplace-apps module before the marketplace_apps uid-mapping is + // written, which starves the downstream GF/CT/entry extension remap. We warn and skip only + // this app's configuration; the app stays installed and its extension mappings are preserved. + // (Skip, not return — the server_configuration block below is independent and must still run.) try { - decryptedConfiguration = this.nodeCrypto.decrypt(configuration); - } catch (error: any) { - log.warn( - `Failed to decrypt configuration for '${appName}'; skipping its configuration update. The app is installed and its extension mappings are preserved. (${ - error?.message || error - })`, - this.importConfig.context, - ); - decryptedConfiguration = undefined; - } - - if (decryptedConfiguration !== undefined) { + const decryptedConfiguration = this.nodeCrypto.decrypt(configuration); await this.appSdk .marketplace(this.importConfig.org_uid) .installation(installation_uid) @@ -735,27 +723,22 @@ export default class ImportMarketplaceApps extends BaseClass { log.error(formatError(error), this.importConfig.context); log.debug(`Configuration update failed for: ${appName}`, this.importConfig.context); }); - } - } - - if (!isEmpty(server_configuration)) { - log.debug(`Updating server configuration for: ${appName}`, this.importConfig.context); - // NOTE: guard the decrypt for the same reason as `configuration` above — a bad-decrypt must - // not abort the module or skip the uid-mapping write. - let decryptedServerConfiguration: any; - try { - decryptedServerConfiguration = this.nodeCrypto.decrypt(server_configuration); } catch (error: any) { log.warn( - `Failed to decrypt server configuration for '${appName}'; skipping its server configuration update. The app is installed and its extension mappings are preserved. (${ + `Failed to decrypt configuration for '${appName}'; skipping its configuration update. The app is installed and its extension mappings are preserved. (${ error?.message || error })`, this.importConfig.context, ); - decryptedServerConfiguration = undefined; } + } - if (decryptedServerConfiguration !== undefined) { + if (!isEmpty(server_configuration)) { + log.debug(`Updating server configuration for: ${appName}`, this.importConfig.context); + // Same rationale as `configuration` above — a bad-decrypt must not abort the module or skip + // the uid-mapping write. + try { + const decryptedServerConfiguration = this.nodeCrypto.decrypt(server_configuration); await this.appSdk .marketplace(this.importConfig.org_uid) .installation(installation_uid) @@ -774,6 +757,13 @@ export default class ImportMarketplaceApps extends BaseClass { log.error(formatError(error), this.importConfig.context); log.debug(`Server configuration update failed for: ${appName}`, this.importConfig.context); }); + } catch (error: any) { + log.warn( + `Failed to decrypt server configuration for '${appName}'; skipping its server configuration update. The app is installed and its extension mappings are preserved. (${ + error?.message || error + })`, + this.importConfig.context, + ); } } }