Skip to content

Commit a641337

Browse files
committed
fix: support Socket CLI v2 settings directory layout
The Socket CLI v2 writes credentials to settings/config.json (a directory) instead of a flat settings file. The scanner now checks both locations, trying the legacy flat file first and falling back to the directory layout. Extracted resolveApiKey() into its own module for testability and added tests covering env variable, legacy flat file, CLI v2 directory layout, precedence, missing settings, and malformed settings. Fixes #9
1 parent 1bac8d8 commit a641337

File tree

3 files changed

+180
-38
lines changed

3 files changed

+180
-38
lines changed

src/index.ts

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,9 @@
11
import Bun from 'bun'
2-
import path from 'node:path'
3-
import os from 'node:os'
42
import authenticated from './modes/authenticated'
53
import unauthenticated from './modes/unauthenticated'
4+
import { resolveApiKey } from './resolve-api-key'
65

7-
let SOCKET_API_KEY = process.env.SOCKET_API_KEY
8-
9-
if (typeof SOCKET_API_KEY !== 'string') {
10-
// get OS app data directory
11-
let dataHome = process.platform === 'win32'
12-
? Bun.env.LOCALAPPDATA
13-
: Bun.env.XDG_DATA_HOME
14-
15-
// fallback
16-
if (!dataHome) {
17-
if (process.platform === 'win32') throw new Error('missing %LOCALAPPDATA%')
18-
19-
const home = os.homedir()
20-
21-
dataHome = path.join(home, ...(process.platform === 'darwin'
22-
? ['Library', 'Application Support']
23-
: ['.local', 'share']
24-
))
25-
}
26-
27-
// append `socket/settings`
28-
const defaultSettingsPath = path.join(dataHome, 'socket', 'settings')
29-
const file = Bun.file(defaultSettingsPath)
30-
31-
// attempt to read token from socket settings
32-
if (await file.exists()) {
33-
const rawContent = await file.text()
34-
// rawContent is base64, must decode
35-
36-
try {
37-
SOCKET_API_KEY = JSON.parse(Buffer.from(rawContent, 'base64').toString().trim()).apiToken
38-
} catch {
39-
throw new Error('error reading Socket settings')
40-
}
41-
}
42-
}
6+
const SOCKET_API_KEY = await resolveApiKey()
437

448
if (!SOCKET_API_KEY) {
459
console.log(`⚠ Socket Security Scanner free mode. Set SOCKET_API_KEY to use your Socket org settings.`)

src/resolve-api-key.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import Bun from 'bun'
2+
import path from 'node:path'
3+
import os from 'node:os'
4+
5+
export async function resolveApiKey (): Promise<string | undefined> {
6+
if (typeof process.env.SOCKET_API_KEY === 'string') {
7+
return process.env.SOCKET_API_KEY
8+
}
9+
10+
// get OS app data directory
11+
let dataHome = process.platform === 'win32'
12+
? Bun.env.LOCALAPPDATA
13+
: Bun.env.XDG_DATA_HOME
14+
15+
// fallback
16+
if (!dataHome) {
17+
if (process.platform === 'win32') throw new Error('missing %LOCALAPPDATA%')
18+
19+
const home = os.homedir()
20+
21+
dataHome = path.join(home, ...(process.platform === 'darwin'
22+
? ['Library', 'Application Support']
23+
: ['.local', 'share']
24+
))
25+
}
26+
27+
// attempt to read token from socket settings
28+
// supports both the legacy flat file and the CLI v2 directory layout
29+
const settingsPath = path.join(dataHome, 'socket', 'settings')
30+
const candidates = [
31+
Bun.file(settingsPath),
32+
Bun.file(path.join(settingsPath, 'config.json'))
33+
]
34+
35+
for (const file of candidates) {
36+
if (await file.exists()) {
37+
const rawContent = await file.text()
38+
// rawContent is base64, must decode
39+
40+
try {
41+
return JSON.parse(Buffer.from(rawContent, 'base64').toString().trim()).apiToken
42+
} catch {
43+
throw new Error('error reading Socket settings')
44+
}
45+
}
46+
}
47+
48+
return undefined
49+
}

test/resolve-api-key.test.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { expect, test, describe, beforeEach, afterEach } from 'bun:test'
2+
import { resolveApiKey } from '../src/resolve-api-key'
3+
import path from 'node:path'
4+
import fs from 'node:fs'
5+
import os from 'node:os'
6+
7+
describe('resolveApiKey', () => {
8+
let tmpDir: string
9+
let originalEnv: Record<string, string | undefined>
10+
11+
beforeEach(() => {
12+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'socket-test-'))
13+
originalEnv = {
14+
SOCKET_API_KEY: process.env.SOCKET_API_KEY,
15+
XDG_DATA_HOME: process.env.XDG_DATA_HOME,
16+
}
17+
})
18+
19+
afterEach(() => {
20+
// restore env
21+
for (const [key, value] of Object.entries(originalEnv)) {
22+
if (value === undefined) {
23+
delete process.env[key]
24+
} else {
25+
process.env[key] = value
26+
}
27+
}
28+
29+
fs.rmSync(tmpDir, { recursive: true, force: true })
30+
})
31+
32+
test('should return SOCKET_API_KEY from environment variable', async () => {
33+
process.env.SOCKET_API_KEY = 'env-test-token'
34+
35+
const result = await resolveApiKey()
36+
37+
expect(result).toBe('env-test-token')
38+
})
39+
40+
test('should read token from legacy flat settings file', async () => {
41+
delete process.env.SOCKET_API_KEY
42+
process.env.XDG_DATA_HOME = tmpDir
43+
44+
const settingsDir = path.join(tmpDir, 'socket')
45+
fs.mkdirSync(settingsDir, { recursive: true })
46+
47+
const token = 'legacy-flat-file-token'
48+
const content = Buffer.from(JSON.stringify({ apiToken: token })).toString('base64')
49+
fs.writeFileSync(path.join(settingsDir, 'settings'), content)
50+
51+
const result = await resolveApiKey()
52+
53+
expect(result).toBe(token)
54+
})
55+
56+
test('should read token from CLI v2 settings/config.json', async () => {
57+
delete process.env.SOCKET_API_KEY
58+
process.env.XDG_DATA_HOME = tmpDir
59+
60+
const settingsDir = path.join(tmpDir, 'socket', 'settings')
61+
fs.mkdirSync(settingsDir, { recursive: true })
62+
63+
const token = 'cli-v2-directory-token'
64+
const content = Buffer.from(JSON.stringify({ apiToken: token })).toString('base64')
65+
fs.writeFileSync(path.join(settingsDir, 'config.json'), content)
66+
67+
const result = await resolveApiKey()
68+
69+
expect(result).toBe(token)
70+
})
71+
72+
test('should prefer legacy flat file over CLI v2 directory', async () => {
73+
delete process.env.SOCKET_API_KEY
74+
process.env.XDG_DATA_HOME = tmpDir
75+
76+
const socketDir = path.join(tmpDir, 'socket')
77+
78+
// create legacy flat file
79+
fs.mkdirSync(socketDir, { recursive: true })
80+
const legacyToken = 'legacy-token'
81+
fs.writeFileSync(
82+
path.join(socketDir, 'settings'),
83+
Buffer.from(JSON.stringify({ apiToken: legacyToken })).toString('base64')
84+
)
85+
86+
// Note: can't have both a file and directory named 'settings',
87+
// so this test just verifies the flat file is read when it exists
88+
89+
const result = await resolveApiKey()
90+
91+
expect(result).toBe(legacyToken)
92+
})
93+
94+
test('should return undefined when no settings exist', async () => {
95+
delete process.env.SOCKET_API_KEY
96+
process.env.XDG_DATA_HOME = tmpDir
97+
98+
const result = await resolveApiKey()
99+
100+
expect(result).toBeUndefined()
101+
})
102+
103+
test('should throw on malformed settings file', async () => {
104+
delete process.env.SOCKET_API_KEY
105+
process.env.XDG_DATA_HOME = tmpDir
106+
107+
const settingsDir = path.join(tmpDir, 'socket')
108+
fs.mkdirSync(settingsDir, { recursive: true })
109+
fs.writeFileSync(path.join(settingsDir, 'settings'), 'not-valid-base64-json!!!')
110+
111+
await expect(resolveApiKey()).rejects.toThrow('error reading Socket settings')
112+
})
113+
114+
test('should prefer env variable over settings file', async () => {
115+
process.env.SOCKET_API_KEY = 'env-takes-priority'
116+
process.env.XDG_DATA_HOME = tmpDir
117+
118+
const settingsDir = path.join(tmpDir, 'socket', 'settings')
119+
fs.mkdirSync(settingsDir, { recursive: true })
120+
fs.writeFileSync(
121+
path.join(settingsDir, 'config.json'),
122+
Buffer.from(JSON.stringify({ apiToken: 'file-token' })).toString('base64')
123+
)
124+
125+
const result = await resolveApiKey()
126+
127+
expect(result).toBe('env-takes-priority')
128+
})
129+
})

0 commit comments

Comments
 (0)