@@ -53,3 +53,169 @@ export function envAsString(value: unknown, defaultValue = ''): string {
5353 }
5454 return StringCtor ( value ) . trim ( )
5555}
56+
57+ /**
58+ * Create a case-insensitive environment variable Proxy for Windows compatibility.
59+ * On Windows, environment variables are case-insensitive (PATH vs Path vs path).
60+ * This Proxy provides consistent access regardless of case, with priority given
61+ * to exact matches, then case-insensitive matches for known vars.
62+ *
63+ * **Use Cases:**
64+ * - Cross-platform test environments needing consistent env var access
65+ * - Windows compatibility when passing env to child processes
66+ * - Merging environment overrides while preserving case-insensitive lookups
67+ *
68+ * **Performance Note:**
69+ * Proxy operations have runtime overhead. Only use when Windows case-insensitive
70+ * access is required. For most use cases, process.env directly is sufficient.
71+ *
72+ * @param base - Base environment object (usually process.env)
73+ * @param overrides - Optional overrides to merge
74+ * @returns Proxy that handles case-insensitive env var access
75+ *
76+ * @example
77+ * // Create a Proxy with overrides
78+ * const env = createEnvProxy(process.env, { NODE_ENV: 'test' })
79+ * console.log(env.PATH) // Works with any case: PATH, Path, path
80+ * console.log(env.NODE_ENV) // 'test'
81+ *
82+ * @example
83+ * // Pass to child process spawn
84+ * import { createEnvProxy } from '@socketsecurity/lib/env'
85+ * import { spawn } from '@socketsecurity/lib/spawn'
86+ *
87+ * spawn('node', ['script.js'], {
88+ * env: createEnvProxy(process.env, { NODE_ENV: 'test' })
89+ * })
90+ */
91+ export function createEnvProxy (
92+ base : NodeJS . ProcessEnv ,
93+ overrides ?: Record < string , string | undefined > ,
94+ ) : NodeJS . ProcessEnv {
95+ // Common environment variables that have case sensitivity issues on Windows.
96+ // These are checked with case-insensitive matching when exact matches fail.
97+ const caseInsensitiveKeys = new Set ( [
98+ 'PATH' ,
99+ 'TEMP' ,
100+ 'TMP' ,
101+ 'HOME' ,
102+ 'USERPROFILE' ,
103+ 'APPDATA' ,
104+ 'LOCALAPPDATA' ,
105+ 'PROGRAMFILES' ,
106+ 'SYSTEMROOT' ,
107+ 'WINDIR' ,
108+ 'COMSPEC' ,
109+ 'PATHEXT' ,
110+ ] )
111+
112+ return new Proxy (
113+ { } ,
114+ {
115+ get ( _target , prop ) {
116+ if ( typeof prop !== 'string' ) {
117+ return undefined
118+ }
119+
120+ // Priority 1: Check overrides for exact match.
121+ if ( overrides && prop in overrides ) {
122+ return overrides [ prop ]
123+ }
124+
125+ // Priority 2: Check base for exact match.
126+ if ( prop in base ) {
127+ return base [ prop ]
128+ }
129+
130+ // Priority 3: Case-insensitive lookup for known keys.
131+ const upperProp = prop . toUpperCase ( )
132+ if ( caseInsensitiveKeys . has ( upperProp ) ) {
133+ // Check overrides with case variations.
134+ if ( overrides ) {
135+ for ( const key of Object . keys ( overrides ) ) {
136+ if ( key . toUpperCase ( ) === upperProp ) {
137+ return overrides [ key ]
138+ }
139+ }
140+ }
141+ // Check base with case variations.
142+ for ( const key of Object . keys ( base ) ) {
143+ if ( key . toUpperCase ( ) === upperProp ) {
144+ return base [ key ]
145+ }
146+ }
147+ }
148+
149+ return undefined
150+ } ,
151+
152+ ownKeys ( _target ) {
153+ const keys = new Set < string > ( [
154+ ...Object . keys ( base ) ,
155+ ...( overrides ? Object . keys ( overrides ) : [ ] ) ,
156+ ] )
157+ return [ ...keys ]
158+ } ,
159+
160+ getOwnPropertyDescriptor ( _target , prop ) {
161+ if ( typeof prop !== 'string' ) {
162+ return undefined
163+ }
164+
165+ // Use the same lookup logic as get().
166+ const value = this . get ?.( _target , prop , _target )
167+ return value !== undefined
168+ ? {
169+ enumerable : true ,
170+ configurable : true ,
171+ writable : true ,
172+ value,
173+ }
174+ : undefined
175+ } ,
176+
177+ has ( _target , prop ) {
178+ if ( typeof prop !== 'string' ) {
179+ return false
180+ }
181+
182+ // Check overrides.
183+ if ( overrides && prop in overrides ) {
184+ return true
185+ }
186+
187+ // Check base.
188+ if ( prop in base ) {
189+ return true
190+ }
191+
192+ // Case-insensitive check.
193+ const upperProp = prop . toUpperCase ( )
194+ if ( caseInsensitiveKeys . has ( upperProp ) ) {
195+ if ( overrides ) {
196+ for ( const key of Object . keys ( overrides ) ) {
197+ if ( key . toUpperCase ( ) === upperProp ) {
198+ return true
199+ }
200+ }
201+ }
202+ for ( const key of Object . keys ( base ) ) {
203+ if ( key . toUpperCase ( ) === upperProp ) {
204+ return true
205+ }
206+ }
207+ }
208+
209+ return false
210+ } ,
211+
212+ set ( _target , prop , value ) {
213+ if ( typeof prop === 'string' && overrides ) {
214+ overrides [ prop ] = value
215+ return true
216+ }
217+ return false
218+ } ,
219+ } ,
220+ ) as NodeJS . ProcessEnv
221+ }
0 commit comments