From 0cc8e605c1d2d170caed9e65496ad267a69371f6 Mon Sep 17 00:00:00 2001 From: "ankitatripathi.mp@gmail.com" Date: Tue, 28 Jul 2026 00:17:20 +0530 Subject: [PATCH 1/3] fix: strip unresolved placeholders on deploy instead of throwing --- src/tools/auth0/handlers/default.ts | 20 +++--- src/tools/utils.ts | 27 ++++++++ test/tools/auth0/handlers/default.tests.ts | 81 +++++++++++++--------- test/tools/utils.test.js | 75 ++++++++++++++++++++ 4 files changed, 162 insertions(+), 41 deletions(-) diff --git a/src/tools/auth0/handlers/default.ts b/src/tools/auth0/handlers/default.ts index 5dc75d748..2c304f38e 100644 --- a/src/tools/auth0/handlers/default.ts +++ b/src/tools/auth0/handlers/default.ts @@ -7,7 +7,7 @@ import { duplicateItems, obfuscateSensitiveValues, stripObfuscatedFieldsFromPayload, - validateNoUnresolvedPlaceholders, + stripUnresolvedPlaceholders, detectInsufficientScopeError, } from '../../utils'; import log from '../../../logger'; @@ -457,12 +457,12 @@ export default class APIHandler { const updateFN = this.getClientFN(this.functions.update); const updatePayload = (() => { const data = stripFields({ ...updateItem }, this.stripUpdateFields); - const stripped = stripObfuscatedFieldsFromPayload( + const obfuscatedStripped = stripObfuscatedFieldsFromPayload( data, this.sensitiveFieldsToObfuscate ); - return validateNoUnresolvedPlaceholders( - stripped as Asset, + return stripUnresolvedPlaceholders( + obfuscatedStripped as Asset, this.type, this.objString(updateItem) ); @@ -488,12 +488,12 @@ export default class APIHandler { const createFunction = this.getClientFN(this.functions.create); const createPayload = (() => { const strippedPayload = stripFields(createItem, this.stripCreateFields); - const stripped = stripObfuscatedFieldsFromPayload( + const obfuscatedStripped = stripObfuscatedFieldsFromPayload( strippedPayload, this.sensitiveFieldsToObfuscate ); - return validateNoUnresolvedPlaceholders( - stripped as Asset, + return stripUnresolvedPlaceholders( + obfuscatedStripped as Asset, this.type, this.objString(createItem) ); @@ -521,12 +521,12 @@ export default class APIHandler { const updateFN = this.getClientFN(this.functions.update); const updatePayload = (() => { const data = stripFields({ ...updateItem }, this.stripUpdateFields); - const stripped = stripObfuscatedFieldsFromPayload( + const obfuscatedStripped = stripObfuscatedFieldsFromPayload( data, this.sensitiveFieldsToObfuscate ); - return validateNoUnresolvedPlaceholders( - stripped as Asset, + return stripUnresolvedPlaceholders( + obfuscatedStripped as Asset, this.type, this.objString(updateItem) ); diff --git a/src/tools/utils.ts b/src/tools/utils.ts index 9e2e04702..7ec552e85 100644 --- a/src/tools/utils.ts +++ b/src/tools/utils.ts @@ -316,6 +316,33 @@ export const stripObfuscatedFieldsFromPayload = ( return newAsset; }; +// Strips unresolved ##...## / @@...@@ placeholders before sending to Auth0. +// Keyword replacement runs first; any remaining placeholder means no mapping was +// provided. The field is dropped so Auth0 preserves its existing value. +export const stripUnresolvedPlaceholders = ( + data: Asset | null, + resourceType: string, + resourceName: string +): Asset | null => { + if (data === null) return data; + + const unresolved = collectUnresolvedPlaceholders(data); + if (unresolved.length === 0) return data; + + const newAsset = { ...data }; + unresolved.forEach(({ path, value }) => { + log.warn( + `Stripping unresolved placeholder for ${resourceType} "${resourceName}" field "${path}": ${value}. ` + + `To use this value, define ${path + .split('.') + .pop() + ?.toUpperCase()} in AUTH0_KEYWORD_REPLACE_MAPPINGS. The existing value on Auth0 will be preserved.` + ); + dotProp.delete(newAsset, path); + }); + return newAsset; +}; + export const detectInsufficientScopeError = async ( fn: Function ): Promise< diff --git a/test/tools/auth0/handlers/default.tests.ts b/test/tools/auth0/handlers/default.tests.ts index cf813f18d..1b1a20c52 100644 --- a/test/tools/auth0/handlers/default.tests.ts +++ b/test/tools/auth0/handlers/default.tests.ts @@ -177,58 +177,77 @@ describe('#default handler', () => { expect(didCreateFunctionGetCalled).to.equal(true); }); - it('should throw when creating an asset with an unresolved placeholder', async () => { + it('should strip all unresolved placeholders and not throw when creating', async () => { + let capturedPayload: object | null = null; + const handler = new mockHandler({ client: mockApiClient, config, type: mockAssetType, functions: { //@ts-ignore - create: async (payload) => payload, + create: async (payload) => { + capturedPayload = payload; + return payload; + }, }, }); - await expect( - handler.processChanges({} as Assets, { - del: [], - update: [], - conflicts: [], - create: [ - { - id: 'some-id', - unresolved_field: '##UNRESOLVED_PLACEHOLDER##', - resolved_field: 'a-real-value', - }, - ], - }) - ).to.be.rejectedWith(/Unresolved placeholder/); + await handler.processChanges({} as Assets, { + del: [], + update: [], + conflicts: [], + create: [ + { + id: 'some-id', + unresolved_field: '##UNRESOLVED_PLACEHOLDER##', + resolved_field: 'a-real-value', + }, + ], + }); + + expect(capturedPayload).to.deep.equal({ + id: 'some-id', + resolved_field: 'a-real-value', + }); }); - it('should throw when updating an asset with an unresolved placeholder', async () => { + it('should strip all unresolved placeholders and not throw when updating', async () => { + let capturedPayload: object | null = null; + const handler = new mockHandler({ client: mockApiClient, config, type: mockAssetType, functions: { //@ts-ignore - update: async (_identifiers, payload) => payload, + update: async (_id, payload) => { + capturedPayload = payload; + return payload; + }, }, }); - await expect( - handler.processChanges({} as Assets, { - del: [], - create: [], - conflicts: [], - update: [ - { - id: 'foo', - secret: '##UNRESOLVED_SECRET##', - non_sensitive_property: 'regular value', + await handler.processChanges({} as Assets, { + del: [], + create: [], + conflicts: [], + update: [ + { + id: 'foo', + options: { + client_secret: '##CONNECTIONS_WAAD_SECRET##', + client_id: 'real-client-id', }, - ], - }) - ).to.be.rejectedWith(/Unresolved placeholder/); + non_sensitive_property: 'regular value', + }, + ], + }); + + expect(capturedPayload).to.deep.equal({ + options: { client_id: 'real-client-id' }, + non_sensitive_property: 'regular value', + }); }); describe('AUTH0_IGNORE_DRY_RUN_FIELDS', () => { diff --git a/test/tools/utils.test.js b/test/tools/utils.test.js index 2111522a9..84101f235 100644 --- a/test/tools/utils.test.js +++ b/test/tools/utils.test.js @@ -698,6 +698,81 @@ describe('#filterExcluded', () => { }); }); }); + + describe('#stripUnresolvedPlaceholders', () => { + it('should strip a nested field containing an unresolved ##...## placeholder', () => { + const asset = { + id: 'con-1', + name: 'my-waad-connection', + options: { + client_id: 'real-client-id', + client_secret: '##CONNECTIONS_WAAD_SECRET##', + domain: 'example.onmicrosoft.com', + }, + }; + + const result = utils.stripUnresolvedPlaceholders(asset, 'connections', 'my-waad-connection'); + expect(result).to.deep.equal({ + id: 'con-1', + name: 'my-waad-connection', + options: { + client_id: 'real-client-id', + domain: 'example.onmicrosoft.com', + }, + }); + }); + + it('should strip a field containing an unresolved @@...@@ placeholder', () => { + const asset = { + id: 'con-2', + options: { + client_secret: '@@CONNECTIONS_WAAD_SECRET@@', + client_id: 'abc', + }, + }; + + const result = utils.stripUnresolvedPlaceholders(asset, 'connections', 'con-2'); + expect(result).to.deep.equal({ + id: 'con-2', + options: { client_id: 'abc' }, + }); + }); + + it('should strip ALL unresolved placeholders across all fields', () => { + const asset = { + id: 'con-3', + options: { + client_secret: '##CONNECTIONS_OIDC_SECRET##', + api_key: '##SOME_API_KEY##', + domain: 'real-domain.com', + }, + }; + + const result = utils.stripUnresolvedPlaceholders(asset, 'connections', 'con-3'); + expect(result).to.deep.equal({ + id: 'con-3', + options: { domain: 'real-domain.com' }, + }); + }); + + it('should not strip a field that has a resolved (real) value', () => { + const asset = { + id: 'con-4', + options: { + client_secret: 'the-real-secret', + client_id: 'abc', + }, + }; + + const result = utils.stripUnresolvedPlaceholders(asset, 'connections', 'con-4'); + expect(result).to.deep.equal(asset); + }); + + it('should return null when input is null', () => { + const result = utils.stripUnresolvedPlaceholders(null, 'connections', 'con-5'); + expect(result).to.be.null; + }); + }); }); describe('#detectInsufficientScopeError', () => { From 5c3506cf46bbcf7562fa0c8f6589939857b7d8fa Mon Sep 17 00:00:00 2001 From: "ankitatripathi.mp@gmail.com" Date: Wed, 29 Jul 2026 21:15:56 +0530 Subject: [PATCH 2/3] fix: derive mapping key name from placeholder value in warn message --- src/tools/utils.ts | 5 +-- test/tools/auth0/handlers/default.tests.ts | 38 ++++++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/tools/utils.ts b/src/tools/utils.ts index 57faabc54..9ed38cf35 100644 --- a/src/tools/utils.ts +++ b/src/tools/utils.ts @@ -333,10 +333,7 @@ export const stripUnresolvedPlaceholders = ( unresolved.forEach(({ path, value }) => { log.warn( `Stripping unresolved placeholder for ${resourceType} "${resourceName}" field "${path}": ${value}. ` + - `To use this value, define ${path - .split('.') - .pop() - ?.toUpperCase()} in AUTH0_KEYWORD_REPLACE_MAPPINGS. The existing value on Auth0 will be preserved.` + `To use this value, define ${value.replace(/^##|##$|^@@|@@$/g, '')} in AUTH0_KEYWORD_REPLACE_MAPPINGS. The existing value on Auth0 will be preserved.` ); dotProp.delete(newAsset, path); }); diff --git a/test/tools/auth0/handlers/default.tests.ts b/test/tools/auth0/handlers/default.tests.ts index 1b1a20c52..30f7598af 100644 --- a/test/tools/auth0/handlers/default.tests.ts +++ b/test/tools/auth0/handlers/default.tests.ts @@ -250,6 +250,44 @@ describe('#default handler', () => { }); }); + it('should strip all unresolved placeholders and not throw when processing conflicts', async () => { + let capturedPayload: object | null = null; + + const handler = new mockHandler({ + client: mockApiClient, + config, + type: mockAssetType, + functions: { + //@ts-ignore + update: async (_id, payload) => { + capturedPayload = payload; + return payload; + }, + }, + }); + + await handler.processChanges({} as Assets, { + del: [], + create: [], + update: [], + conflicts: [ + { + id: 'foo', + options: { + client_secret: '##CONNECTIONS_WAAD_SECRET##', + client_id: 'real-client-id', + }, + non_sensitive_property: 'regular value', + }, + ], + }); + + expect(capturedPayload).to.deep.equal({ + options: { client_id: 'real-client-id' }, + non_sensitive_property: 'regular value', + }); + }); + describe('AUTH0_IGNORE_DRY_RUN_FIELDS', () => { it('should merge user-configured ignore fields with handler defaults', () => { const configWithIgnore = ((key: string) => { From e390fd2f04b3aa5e8d78add3fa9b65da39826a78 Mon Sep 17 00:00:00 2001 From: "ankitatripathi.mp@gmail.com" Date: Wed, 29 Jul 2026 21:18:21 +0530 Subject: [PATCH 3/3] format fix --- src/tools/utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/tools/utils.ts b/src/tools/utils.ts index 9ed38cf35..4cc4dd7f9 100644 --- a/src/tools/utils.ts +++ b/src/tools/utils.ts @@ -333,7 +333,10 @@ export const stripUnresolvedPlaceholders = ( unresolved.forEach(({ path, value }) => { log.warn( `Stripping unresolved placeholder for ${resourceType} "${resourceName}" field "${path}": ${value}. ` + - `To use this value, define ${value.replace(/^##|##$|^@@|@@$/g, '')} in AUTH0_KEYWORD_REPLACE_MAPPINGS. The existing value on Auth0 will be preserved.` + `To use this value, define ${value.replace( + /^##|##$|^@@|@@$/g, + '' + )} in AUTH0_KEYWORD_REPLACE_MAPPINGS. The existing value on Auth0 will be preserved.` ); dotProp.delete(newAsset, path); });