-
Notifications
You must be signed in to change notification settings - Fork 50
Expand file tree
/
Copy pathsdk.mts
More file actions
304 lines (270 loc) · 9.4 KB
/
Copy pathsdk.mts
File metadata and controls
304 lines (270 loc) · 9.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
/**
* Socket SDK utilities for Socket CLI.
* Manages SDK initialization and configuration for API communication.
*
* Authentication:
* - Interactive password prompt for missing tokens
* - Supports environment variable (SOCKET_CLI_API_TOKEN)
* - Validates token format and presence
*
* Proxy Support:
* - Automatic proxy agent selection
* - HTTP/HTTPS proxy configuration
* - Respects SOCKET_CLI_API_PROXY environment variable
*
* SDK Setup:
* - createSocketSdk: Create configured SDK instance
* - getDefaultApiToken: Retrieve API token from config/env
* - getDefaultProxyUrl: Retrieve proxy URL from config/env
* - getPublicApiToken: Get public API token constant
* - setupSdk: Initialize Socket SDK with authentication
*
* User Agent:
* - Automatic user agent generation from package.json
* - Includes CLI version and platform information
*/
import { readFileSync } from 'node:fs'
import { Agent as HttpsAgent } from 'node:https'
import { rootCertificates } from 'node:tls'
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'
import isInteractive from '@socketregistry/is-interactive/index.cjs'
import { debugFn } from '@socketsecurity/registry/lib/debug'
import { logger } from '@socketsecurity/registry/lib/logger'
import { password } from '@socketsecurity/registry/lib/prompts'
import { isNonEmptyString } from '@socketsecurity/registry/lib/strings'
import { isUrl } from '@socketsecurity/registry/lib/url'
import { SocketSdk, createUserAgentFromPkgJson } from '@socketsecurity/sdk'
import { getConfigValueOrUndef } from './config.mts'
import { debugApiRequest, debugApiResponse } from './debug.mts'
import constants, {
CONFIG_KEY_API_BASE_URL,
CONFIG_KEY_API_PROXY,
CONFIG_KEY_API_TOKEN,
} from '../constants.mts'
import { trackCliEvent } from './telemetry/integration.mts'
import type { CResult } from '../types.mts'
import type { RequestInfo, ResponseInfo } from '@socketsecurity/sdk'
// Lazy-evaluated full CLI user agent for direct (non-SDK) API calls.
// Includes the CLI product token, Node.js version, and OS platform/arch.
// e.g. "socket/1.1.96 node/v22.0.0 linux/arm64"
let _cliUserAgent: string | undefined
export function getCliUserAgent(): string {
if (!_cliUserAgent) {
const name = constants.ENV.INLINED_SOCKET_CLI_NAME.replace('@', '').replace(
'/',
'-',
)
const version = constants.ENV.INLINED_SOCKET_CLI_VERSION
_cliUserAgent = [
`${name}/${version}`,
`node/${process.version}`,
`${process.platform}/${process.arch}`,
].join(' ')
}
return _cliUserAgent
}
const TOKEN_PREFIX = 'sktsec_'
const TOKEN_PREFIX_LENGTH = TOKEN_PREFIX.length
const TOKEN_VISIBLE_LENGTH = 5
// The Socket API server that should be used for operations.
export function getDefaultApiBaseUrl(): string | undefined {
const baseUrl =
constants.ENV.SOCKET_CLI_API_BASE_URL ||
getConfigValueOrUndef(CONFIG_KEY_API_BASE_URL)
return isUrl(baseUrl) ? baseUrl : undefined
}
// The Socket API server that should be used for operations.
export function getDefaultProxyUrl(): string | undefined {
const apiProxy =
constants.ENV.SOCKET_CLI_API_PROXY ||
getConfigValueOrUndef(CONFIG_KEY_API_PROXY)
return isUrl(apiProxy) ? apiProxy : undefined
}
// Cached extra CA certificates for SSL_CERT_FILE support.
let _extraCaCerts: string[] | undefined
let _extraCaCertsResolved = false
// Returns combined root and extra CA certificates when SSL_CERT_FILE is set
// but NODE_EXTRA_CA_CERTS is not. Node.js loads NODE_EXTRA_CA_CERTS at process
// startup, so setting SSL_CERT_FILE alone does not affect the current process.
// This function reads the certificate file manually and combines it with the
// default root certificates for use in HTTPS agents.
export function getExtraCaCerts(): string[] | undefined {
if (_extraCaCertsResolved) {
return _extraCaCerts
}
_extraCaCertsResolved = true
// Node.js already loaded extra CA certs at startup.
if (process.env['NODE_EXTRA_CA_CERTS']) {
return undefined
}
// Check for SSL_CERT_FILE fallback via constants.
const certPath = constants.ENV.NODE_EXTRA_CA_CERTS
if (!certPath) {
return undefined
}
try {
const extraCerts = readFileSync(certPath, 'utf-8')
// Combine default root certificates with extra certificates. Specifying ca
// in an agent replaces the default trust store, so both must be included.
_extraCaCerts = [...rootCertificates, extraCerts]
return _extraCaCerts
} catch (e) {
debugFn('warn', `Failed to read certificate file: ${certPath}`, e)
return undefined
}
}
// This Socket API token should be stored globally for the duration of the CLI execution.
let _defaultToken: string | undefined
export function getDefaultApiToken(): string | undefined {
if (constants.ENV.SOCKET_CLI_NO_API_TOKEN) {
_defaultToken = undefined
return _defaultToken
}
const key =
constants.ENV.SOCKET_CLI_API_TOKEN ||
getConfigValueOrUndef(CONFIG_KEY_API_TOKEN) ||
_defaultToken
_defaultToken = isNonEmptyString(key) ? key : undefined
return _defaultToken
}
export function getPublicApiToken(): string {
return (
getDefaultApiToken() ||
constants.ENV.SOCKET_CLI_API_TOKEN ||
constants.SOCKET_PUBLIC_API_TOKEN
)
}
export function getVisibleTokenPrefix(): string {
const apiToken = getDefaultApiToken()
return apiToken
? apiToken.slice(
TOKEN_PREFIX_LENGTH,
TOKEN_PREFIX_LENGTH + TOKEN_VISIBLE_LENGTH,
)
: ''
}
export function hasDefaultApiToken(): boolean {
return !!getDefaultApiToken()
}
export type SetupSdkOptions = {
apiBaseUrl?: string | undefined
apiProxy?: string | undefined
apiToken?: string | undefined
}
export async function setupSdk(
options?: SetupSdkOptions | undefined,
): Promise<CResult<SocketSdk>> {
const opts = { __proto__: null, ...options } as SetupSdkOptions
let { apiToken = getDefaultApiToken() } = opts
if (typeof apiToken !== 'string' && isInteractive()) {
apiToken = await password({
message:
'Enter your Socket.dev API token (not saved, use socket login to persist)',
})
_defaultToken = apiToken
}
if (!apiToken) {
return {
ok: false,
message: 'Auth Error',
cause: 'You need to provide an API token. Run `socket login` first.',
}
}
let { apiProxy } = opts
if (!isUrl(apiProxy)) {
apiProxy = getDefaultProxyUrl()
}
const { apiBaseUrl = getDefaultApiBaseUrl() } = opts
// Usage of HttpProxyAgent vs. HttpsProxyAgent based on the chart at:
// https://github.com/delvedor/hpagent?tab=readme-ov-file#usage
const ProxyAgent = apiBaseUrl?.startsWith('http:')
? HttpProxyAgent
: HttpsProxyAgent
// Load extra CA certificates for SSL_CERT_FILE support when
// NODE_EXTRA_CA_CERTS was not set at process startup.
const ca = getExtraCaCerts()
const sdkOptions = {
...(apiProxy
? {
agent: new ProxyAgent({
proxy: apiProxy,
...(ca ? { ca, proxyConnectOptions: { ca } } : {}),
}),
}
: ca
? { agent: new HttpsAgent({ ca }) }
: {}),
...(apiBaseUrl ? { baseUrl: apiBaseUrl } : {}),
timeout: constants.ENV.SOCKET_CLI_API_TIMEOUT,
userAgent: createUserAgentFromPkgJson({
name: constants.ENV.INLINED_SOCKET_CLI_NAME,
version: constants.ENV.INLINED_SOCKET_CLI_VERSION,
homepage: constants.ENV.INLINED_SOCKET_CLI_HOMEPAGE,
}),
// Add HTTP request hooks for telemetry and debugging.
hooks: {
onRequest: (info: RequestInfo) => {
// Skip tracking for telemetry submission endpoints to prevent infinite loop.
const isTelemetryEndpoint = info.url.includes('/telemetry')
if (constants.ENV.SOCKET_CLI_DEBUG) {
// Debug logging.
debugApiRequest(info.method, info.url, info.timeout)
}
if (!isTelemetryEndpoint) {
// Track API request event.
void trackCliEvent('api_request', process.argv, {
method: info.method,
timeout: info.timeout,
url: info.url,
})
}
},
onResponse: (info: ResponseInfo) => {
// Skip tracking for telemetry submission endpoints to prevent infinite loop.
const isTelemetryEndpoint = info.url.includes('/telemetry')
if (!isTelemetryEndpoint) {
// Track API response event.
const metadata = {
duration: info.duration,
method: info.method,
status: info.status,
statusText: info.statusText,
url: info.url,
}
if (info.error) {
// Track as error event if request failed.
void trackCliEvent('api_error', process.argv, {
...metadata,
error_message: info.error.message,
error_type: info.error.constructor.name,
})
} else {
// Track as successful response.
void trackCliEvent('api_response', process.argv, metadata)
}
}
if (constants.ENV.SOCKET_CLI_DEBUG) {
// Debug logging.
debugApiResponse(
info.method,
info.url,
info.status,
info.error,
info.duration,
info.headers,
)
}
},
},
}
if (constants.ENV.SOCKET_CLI_DEBUG) {
logger.info(
`[DEBUG] ${new Date().toISOString()} SDK options: ${JSON.stringify(sdkOptions)}`,
)
}
const sdk = new SocketSdk(apiToken, sdkOptions)
return {
ok: true,
data: sdk,
}
}