Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/constants.mts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { MapCtor, SetCtor } from '@socketsecurity/lib/primordials/map-set'

import rootPkgJson from '../package.json' with { type: 'json' }
import { createUserAgentFromPkgJson } from './user-agent.mts'
import { buildSdkBaseUserAgent } from './user-agent.mts'

import type { ALERT_ACTION, ALERT_TYPE } from './types.mts'

Expand All @@ -18,7 +18,7 @@ export {
SOCKET_DASHBOARD_URL,
} from '@socketsecurity/lib/constants/socket'

export const DEFAULT_USER_AGENT = createUserAgentFromPkgJson(rootPkgJson)
export const DEFAULT_USER_AGENT = buildSdkBaseUserAgent(rootPkgJson)

// Default timeout for HTTP requests (30 seconds)
export const DEFAULT_HTTP_TIMEOUT = 30_000
Expand Down
4 changes: 3 additions & 1 deletion src/socket-sdk-class.mts
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,9 @@ export class SocketSdk {
...(agent ? { agent } : {}),
headers: {
Authorization: `Basic ${btoa(`${trimmedToken}:`)}`,
'User-Agent': userAgent ?? DEFAULT_USER_AGENT,
'User-Agent': userAgent
? `${DEFAULT_USER_AGENT} ${userAgent}`
: DEFAULT_USER_AGENT,
},
signal: getSdkAbortSignal(),
/* c8 ignore next - Optional timeout parameter, tested implicitly through method calls */
Expand Down
3 changes: 2 additions & 1 deletion src/types.mts
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,8 @@ export interface SocketSdkOptions {
*/
timeout?: number | undefined
/**
* Custom User-Agent header.
* User-Agent token appended to the SDK's base User-Agent (which already
* carries the SDK name/version, Node.js version, and OS platform/arch).
*/
userAgent?: string | undefined
}
Expand Down
28 changes: 27 additions & 1 deletion src/user-agent.mts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,35 @@
* headers from package.json data for API requests.
*/

import os from 'node:os'
import process from 'node:process'

/**
* Build the SDK's base User-Agent string, including the SDK name/version, the
* running Node.js version, and the OS platform and architecture. This is used
* as the default User-Agent for API requests; a caller-supplied token (built
* with `createUserAgentFromPkgJson`) is appended to it rather than replacing
* it.
*
* Example output: `socketsecurity-sdk/4.0.4 node/v24.14.1 linux/arm64`
*/
export function buildSdkBaseUserAgent(pkgData: {
name: string
version: string
}): string {
const name = pkgData.name.replace('@', '').replace('/', '-')
return [
`${name}/${pkgData.version}`,
`node/${process.version}`,
`${os.platform()}/${os.arch()}`,
].join(' ')
}

/**
* Generate a User-Agent string from package.json data. Creates standardized
* User-Agent format with optional homepage URL.
* User-Agent format with optional homepage URL. Pass the result as `userAgent`
* in `SocketSdkOptions` to identify your application; it is appended to the
* SDK's own base User-Agent string.
*/
export function createUserAgentFromPkgJson(pkgData: {
name: string
Expand Down
9 changes: 7 additions & 2 deletions test/repo/unit/getapi-sendapi-methods.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import nock from 'nock'
import { describe, expect, it } from 'vitest'

import { DEFAULT_USER_AGENT } from '../../../src/constants.mts'
import { SocketSdk } from '../../../src/index.mts'
import { setupTestClient } from '../../utils/environment.mts'

Expand Down Expand Up @@ -288,7 +289,7 @@ describe('getApi and sendApi Methods', () => {
}
})

it('should use custom User-Agent from SDK options', async () => {
it('should append custom User-Agent from SDK options to the SDK base', async () => {
const customClient = new SocketSdk('test-token', {
userAgent: 'CustomApp/1.0.0',
})
Expand All @@ -305,7 +306,11 @@ describe('getApi and sendApi Methods', () => {
throws: false,
})

expect(capturedHeaders['user-agent']).toBe('CustomApp/1.0.0')
// The caller token is appended to the enriched SDK base, not swapped in.
expect(capturedHeaders['user-agent']).toBe(
`${DEFAULT_USER_AGENT} CustomApp/1.0.0`,
)
expect(capturedHeaders['user-agent']).toContain('node/')
})

it('should work with custom base URLs', async () => {
Expand Down
48 changes: 47 additions & 1 deletion test/repo/unit/user-agent.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@
* through verbatim.
*/

import os from 'node:os'
import process from 'node:process'

import fc from 'fast-check'
import { describe, expect, test } from 'vitest'

import { createUserAgentFromPkgJson } from '../../../src/user-agent.mts'
import {
buildSdkBaseUserAgent,
createUserAgentFromPkgJson,
} from '../../../src/user-agent.mts'

// A plain (unscoped) package-name fragment: alnum + dashes + dots, no '@'/'/'.
const plainName = fc
Expand Down Expand Up @@ -76,3 +82,43 @@ describe('user-agent/createUserAgentFromPkgJson (fuzz)', () => {
)
})
})

describe('user-agent/buildSdkBaseUserAgent', () => {
// DERIVED-FROM-INPUT: the base UA is the SDK name/version enriched with the
// running Node.js version and the OS platform/arch, space-separated in a
// stable order. Fields are read from node:process / node:os so the expected
// value is knowable at test time.
test('formats "name/version node/<ver> <platform>/<arch>"', () => {
fc.assert(
fc.property(plainName, version, (name, ver) => {
expect(buildSdkBaseUserAgent({ name, version: ver })).toBe(
`${name}/${ver} node/${process.version} ${os.platform()}/${os.arch()}`,
)
}),
)
})

// Enrichment tokens are always present regardless of the pkg fields.
test('includes the node version and the OS platform/arch tokens', () => {
const ua = buildSdkBaseUserAgent({
name: 'socketsecurity-sdk',
version: '4.0.4',
})
expect(ua).toContain(`node/${process.version}`)
expect(ua).toContain(`${os.platform()}/${os.arch()}`)
})

// NORMALIZER: a scoped name is normalized the same way as the caller-token
// builder (@scope/pkg -> scope-pkg).
test('normalizes a scoped @scope/pkg name to scope-pkg', () => {
fc.assert(
fc.property(plainName, plainName, version, (scope, pkg, ver) => {
expect(
buildSdkBaseUserAgent({ name: `@${scope}/${pkg}`, version: ver }),
).toBe(
`${scope}-${pkg}/${ver} node/${process.version} ${os.platform()}/${os.arch()}`,
)
}),
)
})
})
Loading