/ utils / mtls.ts
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  }