@@ -97,6 +97,17 @@ export {
9797 * raw-headers list's value) is carried only when the patch's array equals the
9898 * served array.
9999 *
100+ * ## Only where the read path would still withhold it
101+ *
102+ * Whether a position is withheld can depend on its siblings: the `value` of a
103+ * `{ name, value }` pair is credential material only while a label names a
104+ * credential. A value carried forward beside an EDITED sibling (the label
105+ * renamed, deleted, or moved to another label key) could therefore land where
106+ * the read path would serve it. So the grafted config is redacted again, and a
107+ * graft survives only when the redaction still withholds its landing path —
108+ * repeated until nothing more drops. A dropped graft leaves the patch as the
109+ * author sent it there.
110+ *
100111 * What this does NOT do is let a patch set a refused key: `assertValidConfig`
101112 * still runs on the merged record, so a caller that types `password` into the
102113 * config gets #8078's refusal exactly as it would without this function.
@@ -110,20 +121,48 @@ export function restoreRedactedConfig(
110121 if ( ! stored || typeof stored !== 'object' ) return patch ;
111122
112123 const served = redactDatasourceConfig ( driver , stored ) ;
113- let out : Record < string , unknown > = patch ;
114-
124+ const grafts : Array < { landing : string [ ] ; value : unknown } > = [ ] ;
115125 for ( const path of served . redactedPaths ) {
116126 const storedLeaf = valueAt ( stored , path ) ;
117127 if ( storedLeaf === undefined ) continue ;
118128 const landing = landingPath ( served . config , patch , path ) ;
119- if ( ! landing ) continue ;
120- if ( out === patch ) out = { ...patch } ;
121- graftAt ( out , landing , storedLeaf ) ;
129+ if ( landing ) grafts . push ( { landing, value : storedLeaf } ) ;
122130 }
131+ if ( grafts . length === 0 ) return patch ;
132+
133+ const graftAll = ( kept : readonly { landing : string [ ] ; value : unknown } [ ] ) : Record < string , unknown > => {
134+ const out : Record < string , unknown > = { ...patch } ;
135+ for ( const graft of kept ) graftAt ( out , graft . landing , graft . value ) ;
136+ return out ;
137+ } ;
123138
124- return out ;
139+ // An untouched Save — the patch IS the served projection — grafts every
140+ // withheld value back onto exactly what it was withheld from, so the merged
141+ // config is the stored one and the read path withholds the same positions:
142+ // no second walk is owed.
143+ if ( grafts . length === served . redactedPaths . length && sameValue ( patch , served . config ) ) return graftAll ( grafts ) ;
144+
145+ // Otherwise keep only what the read path would STILL withhold where it
146+ // lands. The judgment of a position can depend on its siblings — a pair's
147+ // `value` is a credential only while a label names one — so a value carried
148+ // under an edited sibling may land where the read path would serve it.
149+ // Each pass re-grafts the survivors onto the untouched patch and drops every
150+ // graft the merged config's redaction no longer withholds AT its landing
151+ // path; the set only shrinks, so this settles within one pass per graft.
152+ // Same loop as the `/meta` carry-forward's (#20590).
153+ let kept = grafts ;
154+ for ( ; ; ) {
155+ const out = graftAll ( kept ) ;
156+ const withheld = new Set ( redactDatasourceConfig ( driver , out ) . redactedPaths . map ( pathKey ) ) ;
157+ const next = kept . filter ( ( graft ) => withheld . has ( pathKey ( graft . landing ) ) ) ;
158+ if ( next . length === kept . length ) return next . length === 0 ? patch : out ;
159+ kept = next ;
160+ }
125161}
126162
163+ /** A path as one comparable string (segments may hold any character, so JSON, not a join). */
164+ const pathKey = ( path : readonly string [ ] ) : string => JSON . stringify ( path ) ;
165+
127166/** An array position, as the redactor spells it in a path (its decimal index). */
128167const isIndex = ( segment : string ) : boolean => / ^ ( 0 | [ 1 - 9 ] [ 0 - 9 ] * ) $ / . test ( segment ) ;
129168
0 commit comments