Skip to content

Commit e4f8c48

Browse files
authored
refactor: migrate to @socketsecurity/lib/primordials (#623)
Swaps direct global usage for primordials across the SDK source. Primordials capture references to JavaScript built-ins (Object.keys, Array.prototype.map, JSON.parse, ...) at module load time, before user code can tamper with prototypes or globals — a hardening tool for code that processes adversarial input. Bumps @socketsecurity/lib 5.24.0 → 5.25.1 to pick up the @socketsecurity/lib/primordials surface (added in 5.25.0). Sites converted (audit found none remaining): src/constants.ts new Set(...) → new SetCtor(...) new Map<...>(...) → new MapCtor(...) with type annotation src/http-client.ts Date.now() → DateNow() (9 sites) new Set(...) → new SetCtor(...) str.trim() → StringPrototypeTrim(str) src/quota-utils.ts new Error(...) → new ErrorCtor(...) (5 sites) src/socket-sdk-class.ts new Error(...) → new ErrorCtor(...) (12 sites) new TypeError(...) → new TypeErrorCtor(...) (2 sites) Array.isArray(x) → ArrayIsArray(x) str.trim() → StringPrototypeTrim(str) (4 sites) src/utils.ts s.toLowerCase() → StringPrototypeToLowerCase(s) new Set(...) → new SetCtor(...) str.trim() → StringPrototypeTrim(str) (multiple) str.endsWith(...) → StringPrototypeEndsWith(str, ...) Promise.withResolvers → PromiseWithResolvers (with cast for generics) new URLSearchParams(...) → new URLSearchParamsCtor(...) (2 sites) src/utils/header-sanitization.ts Array.isArray(x) → ArrayIsArray(x) (2 sites) key.toLowerCase() → StringPrototypeToLowerCase(key) prim audit: surface complete (no gaps), 0 sites remain. prim audit run: \`node /path/to/socket-lib/tools/prim/bin/prim.mts audit --target . --dir src\` Verification: pnpm install ✓ pnpm run check --all ✓ (lint + typecheck pass) pnpm test ✓ 565/565 tests pass
1 parent 9d5f189 commit e4f8c48

8 files changed

Lines changed: 87 additions & 56 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
"@babel/traverse": "7.26.4",
7272
"@babel/types": "7.26.3",
7373
"@oxlint/migrate": "1.52.0",
74-
"@socketsecurity/lib": "5.24.0",
74+
"@socketsecurity/lib": "5.25.1",
7575
"@sveltejs/acorn-typescript": "1.0.8",
7676
"@types/babel__traverse": "7.28.0",
7777
"@types/node": "24.9.2",

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/constants.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
* Provides default values, HTTP agents, and public policy configurations for API interactions.
44
*/
55

6+
import { MapCtor, SetCtor } from '@socketsecurity/lib/primordials'
7+
68
import rootPkgJson from '../package.json' with { type: 'json' }
79
import { createUserAgentFromPkgJson } from './user-agent'
810

@@ -60,10 +62,10 @@ export const SOCKET_FIREWALL_API_URL = 'https://firewall-api.socket.dev/purl'
6062

6163
// https://github.com/sindresorhus/got/blob/v14.4.6/documentation/2-options.md#agent
6264
// Valid HTTP agent names for Got-style agent configuration compatibility.
63-
export const httpAgentNames = new Set(['http', 'https', 'http2'])
65+
export const httpAgentNames = new SetCtor(['http', 'https', 'http2'])
6466

6567
// Public security policy.
66-
export const publicPolicy = new Map<ALERT_TYPE, ALERT_ACTION>([
68+
export const publicPolicy: Map<ALERT_TYPE, ALERT_ACTION> = new MapCtor([
6769
// error (1):
6870
['malware', 'error'],
6971
// warn (7):

src/http-client.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import { isError } from '@socketsecurity/lib/errors'
33
import { httpRequest } from '@socketsecurity/lib/http-request'
44
import { jsonParse } from '@socketsecurity/lib/json/parse'
55
import { perfTimer } from '@socketsecurity/lib/performance'
6+
import {
7+
DateNow,
8+
SetCtor,
9+
StringPrototypeTrim,
10+
} from '@socketsecurity/lib/primordials'
611

712
import {
813
MAX_RESPONSE_SIZE,
@@ -44,7 +49,7 @@ export async function createDeleteRequest(
4449
urlPath: string,
4550
options?: RequestOptionsWithHooks | undefined,
4651
): Promise<HttpResponse> {
47-
const startTime = Date.now()
52+
const startTime = DateNow()
4853
const url = `${baseUrl}${urlPath}`
4954
const method = 'DELETE'
5055
const { hooks, ...rawOpts } = {
@@ -74,7 +79,7 @@ export async function createDeleteRequest(
7479
hooks.onResponse({
7580
method,
7681
url,
77-
duration: Date.now() - startTime,
82+
duration: DateNow() - startTime,
7883
status: response.status,
7984
statusText: response.statusText,
8085
headers: sanitizeHeaders(response.headers),
@@ -87,7 +92,7 @@ export async function createDeleteRequest(
8792
hooks.onResponse({
8893
method,
8994
url,
90-
duration: Date.now() - startTime,
95+
duration: DateNow() - startTime,
9196
error: e as Error,
9297
})
9398
}
@@ -101,7 +106,7 @@ export async function createGetRequest(
101106
urlPath: string,
102107
options?: RequestOptionsWithHooks | undefined,
103108
): Promise<HttpResponse> {
104-
const startTime = Date.now()
109+
const startTime = DateNow()
105110
const url = `${baseUrl}${urlPath}`
106111
const method = 'GET'
107112
const stopTimer = perfTimer('http:get', { urlPath })
@@ -133,7 +138,7 @@ export async function createGetRequest(
133138
hooks.onResponse({
134139
method,
135140
url,
136-
duration: Date.now() - startTime,
141+
duration: DateNow() - startTime,
137142
status: response.status,
138143
statusText: response.statusText,
139144
headers: sanitizeHeaders(response.headers),
@@ -148,7 +153,7 @@ export async function createGetRequest(
148153
hooks.onResponse({
149154
method,
150155
url,
151-
duration: Date.now() - startTime,
156+
duration: DateNow() - startTime,
152157
error: e as Error,
153158
})
154159
}
@@ -164,7 +169,7 @@ export async function createRequestWithJson(
164169
json: unknown,
165170
options?: RequestOptionsWithHooks | undefined,
166171
): Promise<HttpResponse> {
167-
const startTime = Date.now()
172+
const startTime = DateNow()
168173
const url = `${baseUrl}${urlPath}`
169174
const stopTimer = perfTimer(`http:${method.toLowerCase()}`, {
170175
urlPath,
@@ -203,7 +208,7 @@ export async function createRequestWithJson(
203208
hooks.onResponse({
204209
method,
205210
url,
206-
duration: Date.now() - startTime,
211+
duration: DateNow() - startTime,
207212
status: response.status,
208213
statusText: response.statusText,
209214
headers: sanitizeHeaders(response.headers),
@@ -218,7 +223,7 @@ export async function createRequestWithJson(
218223
hooks.onResponse({
219224
method,
220225
url,
221-
duration: Date.now() - startTime,
226+
duration: DateNow() - startTime,
222227
error: e as Error,
223228
})
224229
}
@@ -335,9 +340,10 @@ export function reshapeArtifactForPublicPolicy<
335340
policy?: Map<string, string> | undefined,
336341
): T {
337342
if (!isAuthenticated) {
338-
const allowedActions = actions?.trim()
339-
? new Set(actions.split(','))
340-
: undefined
343+
const allowedActions =
344+
actions !== undefined && StringPrototypeTrim(actions)
345+
? new SetCtor(actions.split(','))
346+
: undefined
341347
const resolvedPolicy = policy ?? defaultPublicPolicy
342348

343349
const reshapeArtifact = (artifact: SocketArtifactWithExtras) => ({

src/quota-utils.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from 'node:fs'
33
import { join } from 'node:path'
44

55
import { memoize, once } from '@socketsecurity/lib/memoization'
6+
import { ErrorCtor } from '@socketsecurity/lib/primordials'
67

78
import type { SocketSdkOperations } from './types'
89

@@ -36,14 +37,14 @@ const loadRequirements = once((): Requirements => {
3637
// Check if the requirements file exists before attempting to read.
3738
/* c8 ignore next 3 - Error path tested in isolation but memoization prevents coverage in main test run */
3839
if (!existsSync(requirementsPath)) {
39-
throw new Error(`Requirements file not found at: ${requirementsPath}`)
40+
throw new ErrorCtor(`Requirements file not found at: ${requirementsPath}`)
4041
}
4142

4243
const data = readFileSync(requirementsPath, 'utf8')
4344
return JSON.parse(data) as Requirements
4445
} catch (e) {
4546
/* c8 ignore next 2 - Error wrapping tested in isolation but memoization prevents coverage in main test run */
46-
throw new Error('Failed to load SDK method requirements', { cause: e })
47+
throw new ErrorCtor('Failed to load SDK method requirements', { cause: e })
4748
}
4849
})
4950

@@ -89,7 +90,7 @@ export const getMethodRequirements = memoize(
8990
const requirement = reqs.api[methodName as string]
9091

9192
if (!requirement) {
92-
throw new Error(`Unknown SDK method: "${String(methodName)}"`)
93+
throw new ErrorCtor(`Unknown SDK method: "${String(methodName)}"`)
9394
}
9495

9596
return {
@@ -149,7 +150,7 @@ export const getQuotaCost = memoize(
149150
const requirement = reqs.api[methodName as string]
150151

151152
if (!requirement) {
152-
throw new Error(`Unknown SDK method: "${String(methodName)}"`)
153+
throw new ErrorCtor(`Unknown SDK method: "${String(methodName)}"`)
153154
}
154155

155156
return requirement.quota
@@ -198,7 +199,7 @@ export const getRequiredPermissions = memoize(
198199
const requirement = reqs.api[methodName as string]
199200

200201
if (!requirement) {
201-
throw new Error(`Unknown SDK method: "${String(methodName)}"`)
202+
throw new ErrorCtor(`Unknown SDK method: "${String(methodName)}"`)
202203
}
203204

204205
return [...requirement.permissions]

src/socket-sdk-class.ts

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ import { debugLog, isDebugNs } from '@socketsecurity/lib/debug'
1313
import { validateFiles } from '@socketsecurity/lib/fs'
1414
import { jsonParse } from '@socketsecurity/lib/json/parse'
1515
import { getOwn, isObjectObject } from '@socketsecurity/lib/objects'
16+
import {
17+
ArrayIsArray,
18+
ErrorCtor,
19+
StringPrototypeTrim,
20+
TypeErrorCtor,
21+
} from '@socketsecurity/lib/primordials'
1622
import { pRetry } from '@socketsecurity/lib/promises'
1723
import { setMaxEventTargetListeners } from '@socketsecurity/lib/suppress-warnings'
1824
import { urlSearchParamAsBoolean } from '@socketsecurity/lib/url'
@@ -144,14 +150,14 @@ export class SocketSdk {
144150
// Input validation for API token.
145151
const MAX_API_TOKEN_LENGTH = 1024
146152
if (typeof apiToken !== 'string') {
147-
throw new TypeError('"apiToken" is required and must be a string')
153+
throw new TypeErrorCtor('"apiToken" is required and must be a string')
148154
}
149-
const trimmedToken = apiToken.trim()
155+
const trimmedToken = StringPrototypeTrim(apiToken)
150156
if (!trimmedToken) {
151-
throw new Error('"apiToken" cannot be empty or whitespace-only')
157+
throw new ErrorCtor('"apiToken" cannot be empty or whitespace-only')
152158
}
153159
if (trimmedToken.length > MAX_API_TOKEN_LENGTH) {
154-
throw new Error(
160+
throw new ErrorCtor(
155161
`"apiToken" exceeds maximum length of ${MAX_API_TOKEN_LENGTH} characters`,
156162
)
157163
}
@@ -177,7 +183,7 @@ export class SocketSdk {
177183
timeout < MIN_HTTP_TIMEOUT ||
178184
timeout > MAX_HTTP_TIMEOUT
179185
) {
180-
throw new TypeError(
186+
throw new TypeErrorCtor(
181187
`"timeout" must be a number between ${MIN_HTTP_TIMEOUT} and ${MAX_HTTP_TIMEOUT} milliseconds`,
182188
)
183189
}
@@ -252,7 +258,7 @@ export class SocketSdk {
252258
// Validate response before processing.
253259
/* c8 ignore next 3 - c8 ignored: because #executeWithRetry always returns a value or throws; res is never undefined in practice */
254260
if (!res) {
255-
throw new Error('Failed to get response from batch PURL request')
261+
throw new ErrorCtor('Failed to get response from batch PURL request')
256262
}
257263
// Parse the newline delimited JSON response.
258264
const isPublicToken = this.#apiToken === SOCKET_PUBLIC_API_TOKEN
@@ -330,15 +336,15 @@ export class SocketSdk {
330336

331337
const preview = responseText.slice(0, 100) || ''
332338
return {
333-
cause: `Please report this. JSON.parse threw an error over the following response: \`${preview.trim()}${responseText.length > 100 ? '…' : ''}\``,
339+
cause: `Please report this. JSON.parse threw an error over the following response: \`${StringPrototypeTrim(preview)}${responseText.length > 100 ? '…' : ''}\``,
334340
data: undefined,
335341
error: 'Server returned invalid JSON',
336342
status: 0,
337343
success: false,
338344
}
339345
}
340346

341-
const errStr = e ? String(e).trim() : ''
347+
const errStr = e ? StringPrototypeTrim(String(e)) : ''
342348
return {
343349
cause: errStr || UNKNOWN_ERROR,
344350
data: undefined,
@@ -386,7 +392,7 @@ export class SocketSdk {
386392
})
387393
/* c8 ignore next 3 - c8 ignored: because pRetry always returns a value or throws; undefined is only possible if the abort signal fires between attempts, which requires precise timing */
388394
if (result === undefined) {
389-
throw new Error('Request aborted')
395+
throw new ErrorCtor('Request aborted')
390396
}
391397
return result
392398
}
@@ -480,14 +486,14 @@ export class SocketSdk {
480486
}
481487
}
482488
if (!(error instanceof ResponseError)) {
483-
throw new Error('Unexpected Socket API error', {
489+
throw new ErrorCtor('Unexpected Socket API error', {
484490
cause: error,
485491
})
486492
}
487493
const { status: statusCode } = error.response
488494
// Throw server errors (5xx) immediately - these are not recoverable client-side.
489495
if (statusCode && statusCode >= 500) {
490-
throw new Error(`Socket API server error (${statusCode})`, {
496+
throw new ErrorCtor(`Socket API server error (${statusCode})`, {
491497
cause: error,
492498
})
493499
}
@@ -523,7 +529,8 @@ export class SocketSdk {
523529
let errorMessage =
524530
error.message ??
525531
/* c8 ignore next - fallback for missing error message */ UNKNOWN_ERROR
526-
const trimmedBody = body?.trim()
532+
const trimmedBody =
533+
body !== undefined ? StringPrototypeTrim(body) : undefined
527534
if (trimmedBody && !errorMessage.includes(trimmedBody)) {
528535
// Replace generic status message with actual error body if present,
529536
// otherwise append the body to the error message.
@@ -656,7 +663,7 @@ export class SocketSdk {
656663
}
657664

658665
// Handle array of values (take first).
659-
const value = Array.isArray(retryAfterValue)
666+
const value = ArrayIsArray(retryAfterValue)
660667
? retryAfterValue[0]
661668
: retryAfterValue
662669

@@ -748,7 +755,7 @@ export class SocketSdk {
748755
// Validate response before processing.
749756
/* c8 ignore next 3 - c8 ignored: because #executeWithRetry always returns a value or throws; res is never undefined in practice */
750757
if (!res) {
751-
throw new Error('Failed to get response from batch PURL request')
758+
throw new ErrorCtor('Failed to get response from batch PURL request')
752759
}
753760
// Parse the newline delimited JSON response.
754761
const results: SocketArtifact[] = []
@@ -795,7 +802,7 @@ export class SocketSdk {
795802
// Validate response before processing.
796803
/* c8 ignore next 3 - c8 ignored: because #executeWithRetry always returns a value or throws; res is never undefined in practice */
797804
if (!res) {
798-
throw new Error('Failed to get response from batch PURL request')
805+
throw new ErrorCtor('Failed to get response from batch PURL request')
799806
}
800807
// Parse the newline delimited JSON response.
801808
const isPublicToken = this.#apiToken === SOCKET_PUBLIC_API_TOKEN
@@ -2066,7 +2073,7 @@ export class SocketSdk {
20662073
'→ Verify: The blob hash is correct.',
20672074
'→ Note: Blob URLs may expire after a certain time period.',
20682075
].join('\n')
2069-
throw new Error(message)
2076+
throw new ErrorCtor(message)
20702077
}
20712078
if (res.status !== 200) {
20722079
const message = [
@@ -2078,7 +2085,7 @@ export class SocketSdk {
20782085
? '→ Try: Retry the download after a short delay.'
20792086
: '→ Verify: The blob hash and URL are correct.',
20802087
].join('\n')
2081-
throw new Error(message)
2088+
throw new ErrorCtor(message)
20822089
}
20832090

20842091
return res.text()
@@ -4400,7 +4407,7 @@ export class SocketSdk {
44004407
return data as PatchViewResponse
44014408
} catch (e) {
44024409
const result = await this.#handleApiError<never>(e)
4403-
throw new Error(result.error, { cause: result.cause })
4410+
throw new ErrorCtor(result.error, { cause: result.cause })
44044411
}
44054412
}
44064413
}

0 commit comments

Comments
 (0)