@@ -121,19 +121,70 @@ export function withRedactedBranch(
121121 // eslint-disable-next-line jsdoc/require-jsdoc
122122 function * redactBranch ( failures : Iterable < Failure > ) : Iterable < Failure > {
123123 for ( const failure of failures ) {
124- yield {
125- ...failure ,
126- // Replace the parent object in the branch with a sanitised copy.
127- // Reference equality (`===`) is intentional: we only want to touch
128- // this specific parent, not an unrelated object at a different depth
129- // that might happen to have the same shape.
130- branch : failure . branch . map ( ( branchItem ) => {
131- if ( branchItem !== parentObj || ! isObject ( parentObj ) ) {
132- return branchItem ;
124+ if ( ! isObject ( parentObj ) ) {
125+ yield failure ;
126+ continue ;
127+ }
128+
129+ // Locate this specific parent by identity. If it isn't in the branch
130+ // (e.g. the failure came from a different scope), pass it through.
131+ const parentIndex = failure . branch . indexOf ( parentObj ) ;
132+ if ( parentIndex === - 1 ) {
133+ yield failure ;
134+ continue ;
135+ }
136+
137+ // Sanitise the parent entry.
138+ const branch = [ ...failure . branch ] ;
139+ branch [ parentIndex ] = redactKeys ( parentObj , sensitiveKeys ) ;
140+
141+ // Walk backward through every ancestor in the branch. Each ancestor
142+ // holds a direct reference to the child below it - without this step,
143+ // outer objects would still expose the unredacted child through their
144+ // own properties (e.g. root.wrapper.secret would remain visible
145+ // even after wrapper was sanitised at branch[1]).
146+ //
147+ // For each level we use the original child reference to find which
148+ // property key points to it (via ===), then replace that entry with
149+ // the already-sanitised child from the patched branch.
150+ for (
151+ let ancestorIndex = parentIndex - 1 ;
152+ ancestorIndex >= 0 ;
153+ ancestorIndex --
154+ ) {
155+ const ancestor = branch [ ancestorIndex ] ;
156+
157+ // No reference to update in this case.
158+ if ( ! isObject ( ancestor ) ) {
159+ break ;
160+ }
161+
162+ // Keep the original child reference, so we can detect it.
163+ const child = failure . branch [ ancestorIndex + 1 ] ;
164+
165+ // The already-sanitised child from the patched branch.
166+ const childSanitized = branch [ ancestorIndex + 1 ] ;
167+
168+ if ( Array . isArray ( ancestor ) ) {
169+ const childIndex = ancestor . indexOf ( child ) ;
170+ if ( childIndex === - 1 ) {
171+ break ;
133172 }
134- return redactKeys ( parentObj , sensitiveKeys ) ;
135- } ) ,
136- } ;
173+ const copy = [ ...ancestor ] ;
174+ copy [ childIndex ] = childSanitized ;
175+ branch [ ancestorIndex ] = copy ;
176+ } else {
177+ const childKey = Object . keys ( ancestor ) . find (
178+ ( key ) => ancestor [ key ] === child ,
179+ ) ;
180+ if ( childKey === undefined ) {
181+ break ;
182+ }
183+ branch [ ancestorIndex ] = { ...ancestor , [ childKey ] : childSanitized } ;
184+ }
185+ }
186+
187+ yield { ...failure , branch } ;
137188 }
138189 }
139190
0 commit comments