@@ -45,3 +45,135 @@ export function deepDiff<T extends unknown>(source: T | undefined, target: T | u
4545 // simple value
4646 return target ;
4747}
48+
49+ /**
50+ * Extracts password field names from a JSON schema
51+ * @param schema The JSON schema object
52+ * @param prefix The current path prefix (for nested schemas)
53+ * @returns Set of field paths that are marked as password type
54+ */
55+ function extractPasswordFields ( schema : unknown , prefix = '' ) : Set < string > {
56+ const passwordFields = new Set < string > ( ) ;
57+
58+ if ( typeof schema !== 'object' || schema === null ) {
59+ return passwordFields ;
60+ }
61+
62+ const schemaObj = schema as Record < string , unknown > ;
63+
64+ // Check if this schema object is marked as password using x-password property
65+ if ( schemaObj [ 'x-password' ] === true ) {
66+ if ( prefix ) {
67+ passwordFields . add ( prefix ) ;
68+ }
69+ }
70+
71+ // Handle patternProperties (for dynamic keys like auth-providers)
72+ if ( schemaObj . patternProperties && typeof schemaObj . patternProperties === 'object' ) {
73+ const patternProps = schemaObj . patternProperties as Record < string , unknown > ;
74+ for ( const subSchema of Object . values ( patternProps ) ) {
75+ const subPasswordFields = extractPasswordFields ( subSchema , prefix ) ;
76+ subPasswordFields . forEach ( ( field ) => passwordFields . add ( field ) ) ;
77+ }
78+ }
79+
80+ // Handle properties (for fixed keys)
81+ if ( schemaObj . properties && typeof schemaObj . properties === 'object' ) {
82+ const properties = schemaObj . properties as Record < string , unknown > ;
83+ for ( const [ key , propSchema ] of Object . entries ( properties ) ) {
84+ const currentPath = prefix ? `${ prefix } .${ key } ` : key ;
85+ const propPasswordFields = extractPasswordFields ( propSchema , currentPath ) ;
86+ propPasswordFields . forEach ( ( field ) => passwordFields . add ( field ) ) ;
87+ }
88+ }
89+
90+ return passwordFields ;
91+ }
92+
93+ /**
94+ * Checks if a field path matches any password field from the schema
95+ * @param fieldPath The field path to check (e.g., "consumerSecret" or "test.consumerSecret")
96+ * @param passwordFields Set of password field paths from schema
97+ * @returns True if the field should be masked
98+ */
99+ function isPasswordField ( fieldPath : string , passwordFields : Set < string > ) : boolean {
100+ // Check exact match
101+ if ( passwordFields . has ( fieldPath ) ) {
102+ return true ;
103+ }
104+
105+ // Check if any password field is a suffix of the current path
106+ // e.g., if schema has "consumerSecret" and path is "test.consumerSecret"
107+ const passwordFieldsArray = Array . from ( passwordFields ) ;
108+ for ( const passwordField of passwordFieldsArray ) {
109+ if ( fieldPath . endsWith ( `.${ passwordField } ` ) || fieldPath === passwordField ) {
110+ return true ;
111+ }
112+ }
113+
114+ return false ;
115+ }
116+
117+ /**
118+ * Masks sensitive values in an object for safe logging
119+ * Fields matching patterns like "secret", "password", "key", "token", etc. will be masked
120+ * Additionally, fields marked as type "password" in the schema will be masked
121+ * @param value The value to mask (can be object, array, or primitive)
122+ * @param keyPath The current key path (for nested objects)
123+ * @param schema Optional JSON schema to check for password type fields
124+ * @returns Masked value safe for logging
125+ */
126+ export function maskSensitiveValues ( value : unknown , keyPath = '' , schema ?: unknown ) : unknown {
127+ // Extract password fields from schema if provided
128+ const passwordFields = schema ? extractPasswordFields ( schema ) : new Set < string > ( ) ;
129+
130+ // List of patterns that indicate sensitive fields (fallback for when schema is not available)
131+ const sensitivePatterns = [
132+ / s e c r e t / i,
133+ / p a s s w o r d / i,
134+ / t o k e n / i,
135+ / k e y / i,
136+ / c r e d e n t i a l / i,
137+ / a u t h / i,
138+ / a p i [ _ - ] ? k e y / i,
139+ / a c c e s s [ _ - ] ? t o k e n / i,
140+ ] ;
141+
142+ const isSensitiveField = ( key : string ) : boolean => {
143+ // First check schema-based password fields
144+ if ( passwordFields . size > 0 && isPasswordField ( key , passwordFields ) ) {
145+ return true ;
146+ }
147+ // Fallback to pattern matching
148+ return sensitivePatterns . some ( ( pattern ) => pattern . test ( key ) ) ;
149+ } ;
150+
151+ if ( value === null || value === undefined ) {
152+ return value ;
153+ }
154+
155+ // If it's a string and we're in a sensitive field context, mask it
156+ if ( typeof value === 'string' && keyPath && isSensitiveField ( keyPath ) ) {
157+ return '****' ;
158+ }
159+
160+ // If it's an object, recursively mask nested values
161+ if ( typeof value === 'object' ) {
162+ if ( Array . isArray ( value ) ) {
163+ return value . map ( ( item , index ) => maskSensitiveValues ( item , `${ keyPath } [${ index } ]` , schema ) ) ;
164+ } else {
165+ const masked : Record < string , unknown > = { } ;
166+ for ( const [ key , val ] of Object . entries ( value ) ) {
167+ const currentPath = keyPath ? `${ keyPath } .${ key } ` : key ;
168+ if ( isSensitiveField ( key ) && typeof val === 'string' && val . length > 0 ) {
169+ masked [ key ] = '****' ;
170+ } else {
171+ masked [ key ] = maskSensitiveValues ( val , currentPath , schema ) ;
172+ }
173+ }
174+ return masked ;
175+ }
176+ }
177+
178+ return value ;
179+ }
0 commit comments