Skip to content

Commit fe6e36b

Browse files
committed
fix: support SSL_CERT_FILE for TLS certificate configuration
Node.js only reads NODE_EXTRA_CA_CERTS at process startup, so setting SSL_CERT_FILE (which the CLI maps to NODE_EXTRA_CA_CERTS internally) had no effect on the parent process's TLS connections. This caused "unable to get local issuer certificate" errors for users behind corporate proxies with SSL inspection (e.g. Cloudflare). The fix manually reads the certificate file and passes the combined CA certificates (root + extra) to HTTPS agents: - SDK calls: HttpsAgent or HttpsProxyAgent with ca option - Direct fetch calls: falls back to node:https.request with custom agent - Child processes (Coana CLI): already worked via constants.processEnv
1 parent 51d1eb7 commit fe6e36b

File tree

4 files changed

+667
-6
lines changed

4 files changed

+667
-6
lines changed

src/utils/api.mts

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
* - Falls back to configured apiBaseUrl or default API_V0_URL
2020
*/
2121

22+
import { Agent as HttpsAgent, request as httpsRequest } from 'node:https'
23+
2224
import { messageWithCauses } from 'pony-cause'
2325

2426
import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug'
@@ -37,7 +39,7 @@ import constants, {
3739
HTTP_STATUS_UNAUTHORIZED,
3840
} from '../constants.mts'
3941
import { getRequirements, getRequirementsKey } from './requirements.mts'
40-
import { getDefaultApiToken } from './sdk.mts'
42+
import { getDefaultApiToken, getExtraCaCerts } from './sdk.mts'
4143

4244
import type { CResult } from '../types.mts'
4345
import type { Spinner } from '@socketsecurity/registry/lib/spinner'
@@ -50,6 +52,81 @@ import type {
5052

5153
const NO_ERROR_MESSAGE = 'No error message returned'
5254

55+
// Cached HTTPS agent for extra CA certificate support in direct API calls.
56+
let _httpsAgent: HttpsAgent | undefined
57+
let _httpsAgentResolved = false
58+
59+
// Returns an HTTPS agent configured with extra CA certificates when
60+
// SSL_CERT_FILE is set but NODE_EXTRA_CA_CERTS is not.
61+
function getHttpsAgent(): HttpsAgent | undefined {
62+
if (_httpsAgentResolved) {
63+
return _httpsAgent
64+
}
65+
_httpsAgentResolved = true
66+
const ca = getExtraCaCerts()
67+
if (!ca) {
68+
return undefined
69+
}
70+
_httpsAgent = new HttpsAgent({ ca })
71+
return _httpsAgent
72+
}
73+
74+
// Wrapper around fetch that supports extra CA certificates via SSL_CERT_FILE.
75+
// Uses node:https.request with a custom agent when extra CA certs are needed,
76+
// falling back to regular fetch() otherwise.
77+
type ApiFetchInit = {
78+
body?: string | undefined
79+
headers?: Record<string, string> | undefined
80+
method?: string | undefined
81+
}
82+
83+
async function apiFetch(url: string, init: ApiFetchInit): Promise<Response> {
84+
const agent = getHttpsAgent()
85+
if (!agent) {
86+
return await fetch(url, init as globalThis.RequestInit)
87+
}
88+
return await new Promise((resolve, reject) => {
89+
const req = httpsRequest(
90+
url,
91+
{
92+
method: init.method || 'GET',
93+
headers: init.headers,
94+
agent,
95+
},
96+
res => {
97+
const chunks: Buffer[] = []
98+
res.on('data', (chunk: Buffer) => chunks.push(chunk))
99+
res.on('end', () => {
100+
const body = Buffer.concat(chunks)
101+
const responseHeaders = new Headers()
102+
for (const [key, value] of Object.entries(res.headers)) {
103+
if (typeof value === 'string') {
104+
responseHeaders.set(key, value)
105+
} else if (Array.isArray(value)) {
106+
for (const v of value) {
107+
responseHeaders.append(key, v)
108+
}
109+
}
110+
}
111+
resolve(
112+
new Response(body, {
113+
status: res.statusCode ?? 0,
114+
statusText: res.statusMessage ?? '',
115+
headers: responseHeaders,
116+
}),
117+
)
118+
})
119+
res.on('error', reject)
120+
},
121+
)
122+
if (init.body) {
123+
req.write(init.body)
124+
}
125+
req.on('error', reject)
126+
req.end()
127+
})
128+
}
129+
53130
export type CommandRequirements = {
54131
permissions?: string[] | undefined
55132
quota?: number | undefined
@@ -287,7 +364,7 @@ async function queryApi(path: string, apiToken: string) {
287364
}
288365

289366
const url = `${baseUrl}${baseUrl.endsWith('/') ? '' : '/'}${path}`
290-
const result = await fetch(url, {
367+
const result = await apiFetch(url, {
291368
method: 'GET',
292369
headers: {
293370
Authorization: `Basic ${btoa(`${apiToken}:`)}`,
@@ -480,7 +557,7 @@ export async function sendApiRequest<T>(
480557
...(body ? { body: JSON.stringify(body) } : {}),
481558
}
482559

483-
result = await fetch(
560+
result = await apiFetch(
484561
`${baseUrl}${baseUrl.endsWith('/') ? '' : '/'}${path}`,
485562
fetchOptions,
486563
)

0 commit comments

Comments
 (0)