116 lines
4.3 KiB
TypeScript
116 lines
4.3 KiB
TypeScript
import memoize from 'lodash-es/memoize.js'
|
|
import { logForDebugging } from './debug.js'
|
|
import { hasNodeOption } from './envUtils.js'
|
|
import { getFsImplementation } from './fsOperations.js'
|
|
|
|
/**
|
|
* Load CA certificates for TLS connections.
|
|
*
|
|
* Since setting `ca` on an HTTPS agent replaces the default certificate store,
|
|
* we must always include base CAs (either system or bundled Mozilla) when returning.
|
|
*
|
|
* Returns undefined when no custom CA configuration is needed, allowing the
|
|
* runtime's default certificate handling to apply.
|
|
*
|
|
* Behavior:
|
|
* - Neither NODE_EXTRA_CA_CERTS nor --use-system-ca/--use-openssl-ca set: undefined (runtime defaults)
|
|
* - NODE_EXTRA_CA_CERTS only: bundled Mozilla CAs + extra cert file contents
|
|
* - --use-system-ca or --use-openssl-ca only: system CAs
|
|
* - --use-system-ca + NODE_EXTRA_CA_CERTS: system CAs + extra cert file contents
|
|
*
|
|
* Memoized for performance. Call clearCACertsCache() to invalidate after
|
|
* environment variable changes (e.g., after trust dialog applies settings.json).
|
|
*
|
|
* Reads ONLY `process.env.NODE_EXTRA_CA_CERTS`. `caCertsConfig.ts` populates
|
|
* that env var from settings.json at CLI init; this module stays config-free
|
|
* so `proxy.ts`/`mtls.ts` don't transitively pull in the command registry.
|
|
*/
|
|
export const getCACertificates = memoize((): string[] | undefined => {
|
|
const useSystemCA =
|
|
hasNodeOption('--use-system-ca') || hasNodeOption('--use-openssl-ca')
|
|
|
|
const extraCertsPath = process.env.NODE_EXTRA_CA_CERTS
|
|
|
|
logForDebugging(
|
|
`CA certs: useSystemCA=${useSystemCA}, extraCertsPath=${extraCertsPath}`,
|
|
)
|
|
|
|
// If neither is set, return undefined (use runtime defaults, no override)
|
|
if (!useSystemCA && !extraCertsPath) {
|
|
return undefined
|
|
}
|
|
|
|
// Deferred load: Bun's node:tls module eagerly materializes ~150 Mozilla
|
|
// root certificates (~750KB heap) on import, even if tls.rootCertificates
|
|
// is never accessed. Most users hit the early return above, so we only
|
|
// pay this cost when custom CA handling is actually needed.
|
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
const tls = require('tls') as typeof import('tls')
|
|
/* eslint-enable @typescript-eslint/no-require-imports */
|
|
|
|
const certs: string[] = []
|
|
|
|
if (useSystemCA) {
|
|
// Load system CA store (Bun API)
|
|
const getCACerts = (
|
|
tls as typeof tls & { getCACertificates?: (type: string) => string[] }
|
|
).getCACertificates
|
|
const systemCAs = getCACerts?.('system')
|
|
if (systemCAs && systemCAs.length > 0) {
|
|
certs.push(...systemCAs)
|
|
logForDebugging(
|
|
`CA certs: Loaded ${certs.length} system CA certificates (--use-system-ca)`,
|
|
)
|
|
} else if (!getCACerts && !extraCertsPath) {
|
|
// Under Node.js where getCACertificates doesn't exist and no extra certs,
|
|
// return undefined to let Node.js handle --use-system-ca natively.
|
|
logForDebugging(
|
|
'CA certs: --use-system-ca set but system CA API unavailable, deferring to runtime',
|
|
)
|
|
return undefined
|
|
} else {
|
|
// System CA API returned empty or unavailable; fall back to bundled root certs
|
|
certs.push(...tls.rootCertificates)
|
|
logForDebugging(
|
|
`CA certs: Loaded ${certs.length} bundled root certificates as base (--use-system-ca fallback)`,
|
|
)
|
|
}
|
|
} else {
|
|
// Must include bundled Mozilla CAs as base since ca replaces defaults
|
|
certs.push(...tls.rootCertificates)
|
|
logForDebugging(
|
|
`CA certs: Loaded ${certs.length} bundled root certificates as base`,
|
|
)
|
|
}
|
|
|
|
// Append extra certs from file
|
|
if (extraCertsPath) {
|
|
try {
|
|
const extraCert = getFsImplementation().readFileSync(extraCertsPath, {
|
|
encoding: 'utf8',
|
|
})
|
|
certs.push(extraCert)
|
|
logForDebugging(
|
|
`CA certs: Appended extra certificates from NODE_EXTRA_CA_CERTS (${extraCertsPath})`,
|
|
)
|
|
} catch (error) {
|
|
logForDebugging(
|
|
`CA certs: Failed to read NODE_EXTRA_CA_CERTS file (${extraCertsPath}): ${error}`,
|
|
{ level: 'error' },
|
|
)
|
|
}
|
|
}
|
|
|
|
return certs.length > 0 ? certs : undefined
|
|
})
|
|
|
|
/**
|
|
* Clear the CA certificates cache.
|
|
* Call this when environment variables that affect CA certs may have changed
|
|
* (e.g., NODE_EXTRA_CA_CERTS, NODE_OPTIONS).
|
|
*/
|
|
export function clearCACertsCache(): void {
|
|
getCACertificates.cache.clear?.()
|
|
logForDebugging('Cleared CA certificates cache')
|
|
}
|