mtls.ts
1 import type * as https from 'https' 2 import { Agent as HttpsAgent } from 'https' 3 import memoize from 'lodash-es/memoize.js' 4 import type * as tls from 'tls' 5 import type * as undici from 'undici' 6 import { getCACertificates } from './caCerts.js' 7 import { logForDebugging } from './debug.js' 8 import { getFsImplementation } from './fsOperations.js' 9 10 export type MTLSConfig = { 11 cert?: string 12 key?: string 13 passphrase?: string 14 } 15 16 export type TLSConfig = MTLSConfig & { 17 ca?: string | string[] | Buffer 18 } 19 20 /** 21 * Get mTLS configuration from environment variables 22 */ 23 export const getMTLSConfig = memoize((): MTLSConfig | undefined => { 24 const config: MTLSConfig = {} 25 26 // Note: NODE_EXTRA_CA_CERTS is automatically handled by Node.js at runtime 27 // We don't need to manually load it - Node.js appends it to the built-in CAs automatically 28 29 // Client certificate 30 if (process.env.CLAUDE_CODE_CLIENT_CERT) { 31 try { 32 config.cert = getFsImplementation().readFileSync( 33 process.env.CLAUDE_CODE_CLIENT_CERT, 34 { encoding: 'utf8' }, 35 ) 36 logForDebugging( 37 'mTLS: Loaded client certificate from CLAUDE_CODE_CLIENT_CERT', 38 ) 39 } catch (error) { 40 logForDebugging(`mTLS: Failed to load client certificate: ${error}`, { 41 level: 'error', 42 }) 43 } 44 } 45 46 // Client key 47 if (process.env.CLAUDE_CODE_CLIENT_KEY) { 48 try { 49 config.key = getFsImplementation().readFileSync( 50 process.env.CLAUDE_CODE_CLIENT_KEY, 51 { encoding: 'utf8' }, 52 ) 53 logForDebugging('mTLS: Loaded client key from CLAUDE_CODE_CLIENT_KEY') 54 } catch (error) { 55 logForDebugging(`mTLS: Failed to load client key: ${error}`, { 56 level: 'error', 57 }) 58 } 59 } 60 61 // Key passphrase 62 if (process.env.CLAUDE_CODE_CLIENT_KEY_PASSPHRASE) { 63 config.passphrase = process.env.CLAUDE_CODE_CLIENT_KEY_PASSPHRASE 64 logForDebugging('mTLS: Using client key passphrase') 65 } 66 67 // Only return config if at least one option is set 68 if (Object.keys(config).length === 0) { 69 return undefined 70 } 71 72 return config 73 }) 74 75 /** 76 * Create an HTTPS agent with mTLS configuration 77 */ 78 export const getMTLSAgent = memoize((): HttpsAgent | undefined => { 79 const mtlsConfig = getMTLSConfig() 80 const caCerts = getCACertificates() 81 82 if (!mtlsConfig && !caCerts) { 83 return undefined 84 } 85 86 const agentOptions: https.AgentOptions = { 87 ...mtlsConfig, 88 ...(caCerts && { ca: caCerts }), 89 // Enable keep-alive for better performance 90 keepAlive: true, 91 } 92 93 logForDebugging('mTLS: Creating HTTPS agent with custom certificates') 94 return new HttpsAgent(agentOptions) 95 }) 96 97 /** 98 * Get TLS options for WebSocket connections 99 */ 100 export function getWebSocketTLSOptions(): tls.ConnectionOptions | undefined { 101 const mtlsConfig = getMTLSConfig() 102 const caCerts = getCACertificates() 103 104 if (!mtlsConfig && !caCerts) { 105 return undefined 106 } 107 108 return { 109 ...mtlsConfig, 110 ...(caCerts && { ca: caCerts }), 111 } 112 } 113 114 /** 115 * Get fetch options with TLS configuration (mTLS + CA certs) for undici 116 */ 117 export function getTLSFetchOptions(): { 118 tls?: TLSConfig 119 dispatcher?: undici.Dispatcher 120 } { 121 const mtlsConfig = getMTLSConfig() 122 const caCerts = getCACertificates() 123 124 if (!mtlsConfig && !caCerts) { 125 return {} 126 } 127 128 const tlsConfig: TLSConfig = { 129 ...mtlsConfig, 130 ...(caCerts && { ca: caCerts }), 131 } 132 133 if (typeof Bun !== 'undefined') { 134 return { tls: tlsConfig } 135 } 136 logForDebugging('TLS: Created undici agent with custom certificates') 137 // Create a custom undici Agent with TLS options. Lazy-required so that 138 // the ~1.5MB undici package is only loaded when mTLS/CA certs are configured. 139 // eslint-disable-next-line @typescript-eslint/no-require-imports 140 const undiciMod = require('undici') as typeof undici 141 const agent = new undiciMod.Agent({ 142 connect: { 143 cert: tlsConfig.cert, 144 key: tlsConfig.key, 145 passphrase: tlsConfig.passphrase, 146 ...(tlsConfig.ca && { ca: tlsConfig.ca }), 147 }, 148 pipelining: 1, 149 }) 150 151 return { dispatcher: agent } 152 } 153 154 /** 155 * Clear the mTLS configuration cache. 156 */ 157 export function clearMTLSCache(): void { 158 getMTLSConfig.cache.clear?.() 159 getMTLSAgent.cache.clear?.() 160 logForDebugging('Cleared mTLS configuration cache') 161 } 162 163 /** 164 * Configure global Node.js TLS settings 165 */ 166 export function configureGlobalMTLS(): void { 167 const mtlsConfig = getMTLSConfig() 168 169 if (!mtlsConfig) { 170 return 171 } 172 173 // NODE_EXTRA_CA_CERTS is automatically handled by Node.js at runtime 174 if (process.env.NODE_EXTRA_CA_CERTS) { 175 logForDebugging( 176 'NODE_EXTRA_CA_CERTS detected - Node.js will automatically append to built-in CAs', 177 ) 178 } 179 }